Concepts / Declarator Operators and Declaration Reading

Declarator Operators and Declaration Reading

A declaration is easiest to read by starting at the name and moving outward through each declarator operator.

  • Programming

A Read-Only Function Over Large Data

Many functions only need to look at the data they receive. A function may compute a total, search for a value, or print a report, and it never needs to change the caller's data. Even so, how an argument is passed is a real design decision, because the passing style determines both what the function is allowed to do to the argument and how much work each call performs. This article follows one small example, a function that adds up the numbers in a vector, and uses it to examine a tool built for exactly this situation: the const reference.

What do you think happens?

If the body of a sum function that receives a const reference to a vector tries to write to one of the elements, will the function compile?

  • Yes, it compiles and changes the caller's vector

  • No, the write is rejected at compile time

Reveal answer
Answer:

No, the write is rejected at compile time

The const in the parameter declaration says the referred-to object cannot be modified through this name. That restriction is enforced by the compiler at the line that tries to modify the parameter, so the program does not build.

Copying Versus Sharing the Argument

The choice of a const reference is a tradeoff between two goals. The first goal is to keep the function from changing the argument. The second is to avoid the cost of copying it. Passing by value gives the function its own copy, which protects the caller's data but costs the work of duplicating it. Passing by non-const reference avoids that copying, but on its own it allows the function to modify the original. Adding const to the reference keeps the saving and removes the ability to change the data.

caller vectororiginal databy value parameterseparate copyby referenceparameteralias, writes allowedby const referenceparameteralias, writes blockedcopied on callsharedshared, read only
When sum is called, does the parameter hold a separate copy of the vector, or an alias to the caller's vector? The three passing styles answer this differently.

The diagram shows the single path the data takes. A const reference parameter does not hold a separate copy of the vector. Reads go through to the caller's data, and writes are blocked at compile time. This makes the const reference a promise enforced by the language rather than a convention the programmer has to remember.

Anatomy of the sum Declaration

The source describes a declaration of the form double sum(const vector<double>&). Every part of that line has a job. The word double is the return type, the name sum identifies the function, and the parameter list contains one parameter whose type is written as const vector<double>&. The diagram below labels each token inside that declaration.

doublereturn typesumfunction nameconstobject cannot changethrough this namevector<double>type being referred to&makes the parameter areference
Which token is the return type, which is the function name, and which tokens modify the parameter? Each token contributes a specific piece of meaning.

Tracing the sum Function

Start with a function that adds up the numbers in a vector. Its parameter is a const reference to a vector of doubles. Read the code as a plain description of behavior: the function reads each element, accumulates a running total, and returns it. The code below is an illustrative example written to match that description.

Code walkthrough
using std::vector;

double sum(const vector<double>& values) {
    double total = 0.0;
    for (size_t i = 0; i < values.size(); ++i) {
        total += values[i];
    }
    return total;
}
  1. 1
    Line 1 brings the vector name into scope so the declaration can say vector<double>.
  2. 2
    Line 3 is the declaration from the previous section: double return type, sum name, const reference parameter.
  3. 3
    Line 4 creates a local running total initialized to zero.
  4. 4
    Lines 5 to 7 read each element and add it to total. Reading values[i] is allowed through a const reference.
  5. 5
    Line 8 returns the accumulated total to the caller.
  • The parameter values is a reference, so no copy of the vector is made when sum is called.
  • The const keyword means values cannot be modified through this name.
  • The loop reads each element and accumulates it into total.
Worked example

Working Out the Result of sum

What does sum return when it is called with the vector 1.5, 2.5, 3.0?

  1. 1
    Start the total

    total begins at 0.0.

  2. 2
    Add the first element

    total becomes 0.0 + 1.5, which is 1.5.

  3. 3
    Add the second element

    total becomes 1.5 + 2.5, which is 4.0.

  4. 4
    Add the third element

    total becomes 4.0 + 3.0, which is 7.0.

  5. 5
    Return

    The loop ends and sum returns the accumulated total.

Result

sum returns 7.0. The function only read the vector, so the caller's data is unchanged.

Editing the Body to Write Through the Name

Now suppose someone edits the body so that it tries to change the vector it was given. The line added below assigns a new value to the first element through the const reference. Before reading the answer, consider what the compiler should do with that line, and then compare the two versions side by side.

Before
double sum(const vector<double>& values) {
    double total = 0.0;
    for (size_t i = 0; i < values.size(); ++i) {
        total += values[i];
    }
    return total;
}

The parameter is a const reference. The body only reads elements and returns a total.

After
double sum(const vector<double>& values) {
    double total = 0.0;
    for (size_t i = 0; i < values.size(); ++i) {
        total += values[i];
    }
    values[0] = 0.0;
    return total;
}

The added assignment writes through the const reference, so the program fails to compile at that line.

The loop and the return statement behave exactly as before. The divergence from the intended behavior is located at the single modifying line. The compiler enforces the restriction at that statement, so the edited version never produces a program that computes a total.

call sum with vcaller passes its vectorbind parameterconst reference aliasread values[i]reads are allowedmodifies const ref?checked per statementreturn totalcompilescompile errorat the modifying linenext statementnoyes
Following the statements of the edited body, which step is the one where the const reference restriction is enforced?

Finding the Line That Fails

When a const reference version of a function fails, the fastest route is to locate the exact line where the behavior stops matching what you intended. Ask which statement attempts to modify the object the parameter refers to. In the sum example, that is the assignment to values[0]. Everything else in the body reads the data, which the const reference allows.

  1. 1
    Identify the parameter and confirm that its declaration includes const.
  2. 2
    Scan the body for any statement that assigns to, or otherwise modifies, that parameter or its elements.
  3. 3
    Mark the first such statement. The restriction is enforced at that line.
  4. 4
    Confirm that reads such as values[i] and values.size() are still permitted, because only modification is blocked.

Choosing a Passing Style

Once the mechanics are clear, the design choice is usually straightforward. The table compares the three styles on the two properties that drive the decision: whether the call copies the argument, and whether the function is allowed to change the caller's data.

Passing styleCopies the argument?Can the function modify the caller's data?Best fit
By valueYes, the function gets its own copyNo, changes affect only the copySmall values, or when the function needs its own working copy
By non-const referenceNo, the function shares the caller's objectYes, changes reach the caller's objectWhen the function is meant to modify its argument
By const referenceNo, the function shares the caller's objectNo, writes are rejected at compile timeRead-only functions taking large arguments

Three ways to pass an argument, compared on copying and modifiability

Common Mistakes with const References

  • Common mistake

    Believing a const reference parameter makes a copy of the vector

    What it looks like

    Assuming sum(const vector<double>&) duplicates the caller's data before the loop runs.

    Why it fails

    The & makes the parameter a reference, so the parameter is an alias to the caller's vector and no copy is made.

    Do this instead

    Think of the parameter as another name for the caller's object, with writes blocked by const.

  • Common mistake

    Reading the ampersand as address-of inside a declaration

    What it looks like

    Reading const vector<double>& as taking the address of a vector.

    Why it fails

    In a declaration, & is a declarator operator that builds a reference type rather than performing an address operation.

    Do this instead

    Read T& as a reference to T when it appears in a parameter or variable declaration.

  • Common mistake

    Expecting the error on the first line of the function

    What it looks like

    Looking only at the return statement when the compile error is reported for values[0] = 0.0.

    Why it fails

    The restriction is enforced at the statement that modifies the parameter, not wherever the function ends.

    Do this instead

    Scan the body for the first statement that writes through the parameter.

  • Common mistake

    Assuming const blocks reading as well

    What it looks like

    Removing the loop because the parameter is const.

    Why it fails

    const only blocks modification through the name. Reads such as values[i] and values.size() remain allowed.

    Do this instead

    Keep reads, and remove or move only the statements that modify the parameter.

  • Common mistake

    Choosing const reference for a function that must change its argument, which defeats the purpose of the call

Practice Reading the Declaration

MEDIUM

Consider the function double first(const vector<double>& values) whose body contains two statements: values[0] = 1.0; followed by return values[0];. Which statement fails to compile, and why does the compiler reject it there? Then state whether the function would be a good candidate for a const reference parameter.

Hints
  • Find the statement that writes through the parameter.
  • Reads such as return values[0]; are allowed through a const reference.
  • A function that only reads a large argument is the case const references are designed for, even if it is short.

Key Takeaways

  • A const reference lets a function read an argument without copying it, while forbidding changes to the original variable through that name.
  • Passing by value copies the data and protects the caller, passing by non-const reference avoids copying but allows changes, and passing by const reference avoids copying and blocks changes.
  • In a declaration, & is a declarator operator that builds a reference type, so T& means a reference to T, and const modifies the object that the reference refers to.
  • Read a declaration from the name outward, applying each declarator operator to the type built so far.
  • When a const reference function fails to compile, find the exact line that modifies the parameter, because the restriction is enforced at that statement.

Related concepts

More concepts starting with D