Const References
The unary suffix & in a declaration means reference to, and a reference is another name for an existing object.
Why a Function Should Not Always Get a Copy
Suppose you own a vector of numbers called my_vec, and you want a sorting routine to rearrange it. The routine needs to touch the vector you actually own, not a duplicate that it throws away when it finishes. If the function receives a copy, the sorting happens to the copy, and your original stays unchanged. This is the problem that references solve.
Many functions do not need to rearrange anything. They compute a total, search for a value, or print a report, and they never need to change the caller's data. Even so, the way an argument is passed still matters, because it determines both what the function is allowed to do to the data and how much work the call performs. This article focuses on the tool for the read-only case: the const reference, which avoids copying while forbidding changes. Keep one question in mind as you read: when a function works on a name that refers to my_vec, is it working on my_vec itself or on something that merely looks like it?
Tracing a Name Back to Its Object
A reference is a second name for an object that already exists. It is introduced with a declaration that places the unary suffix & after the type. The suffix is read as 'reference to', so a declaration such as int& r says that r is a reference to an int.
- int x = 10 creates the object that both names will lead to
- int& r = x declares r as a reference to int and initializes it to name x
- r = 12 is an assignment through the name r, and it writes into x
The declaration must name the object the reference will stand for, because a reference is always tied to something at the moment it is created. This is why the initializer after the equals sign is not optional: a reference without an object to stand for has no meaning. Once the two names are in place, r and x are not two separate values in memory. There is one object, and the two names are two routes to it. Any change made through either name is visible through the other, because both names lead to the same storage.
Why a Reference Stays Bound to One Object
The initializer also decides the reference's identity for life. A reference cannot be made to refer to a different object after its initialization. Writing r = 25 does not rebind r to some other variable. It assigns 25 into the object that r already refers to, which is x. The binding stays the same, and only the value inside the object changes.
This fixed binding is useful for reading code. When you see a reference, you can rely on its name always standing for the same object for its whole lifetime. You do not need to track whether it has been pointed somewhere else.
Reading the Declaration Piece by Piece
The declaration double sum(const vector<double>&) has several parts, and each one has a job. The keyword const says the referred-to object cannot be modified through this name. The type vector<double> names what is being referred to. The ampersand makes the parameter a reference rather than a copy. The leading double is the return type, so it describes the value the function gives back, not the parameter.
Reading a declaration from the inside out helps. Start at the name, then move outward through each declarator operator. For const vector<double>&, the reference operator applies to the whole const vector<double> type, which is why the parameter is described as a reference to a constant vector of doubles.
A declarator operator is an operator such as &, the asterisk, or the square brackets when it appears in a declaration. In that position it does not perform arithmetic or indexing. It shapes the type being declared, so T& is a reference to T.
Where the Const Rule Is Enforced
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 reference avoids copying the vector, and const keeps the function from changing it.
- const vector<double>& v: read-only reference to the caller's vector
- total accumulates the elements as the loop reads them
- the loop only reads elements, which the const reference allows
Now suppose someone edits the body so that it tries to change the vector it was given. Before you read on, predict what happens when the code is compiled.
v.push_back(1.0)is the modifying line- the loop and the return statement are unchanged from the working version
The compiler rejects the modifying line. The const on the parameter is enforced there, not in the loop or the return statement, which behave exactly as before. When a const reference version fails, the fastest route is to locate the exact line where the behavior stops matching what you intended. In this example the divergence is the single line that changes v, and removing it restores the function that only reads.
The const reference is therefore a promise enforced by the language rather than a convention the programmer has to remember. Reads go through to the caller's data, and writes are blocked at compile time.
Choosing a Passing Mode for Read-Only Data
Passing by value copies the data and protects the caller, but it costs the work of duplicating it. Passing by non-const reference avoids copying but allows changes to the original. Passing by const reference avoids copying and blocks changes. For a function that only reads its argument, the choice comes down to two questions: does the function need to change the caller's data, and is the argument large enough that copying would be costly?
| Passing mode | Copies the data | Can modify the caller's object | Typical fit for a read-only function |
|---|---|---|---|
| By value | Yes | No, changes stay in the copy | Small data where copying is cheap |
| By non-const reference | No | Yes | Functions that must change the caller's data, such as sorting |
| By const reference | No | No, writes are rejected at compile time | Large read-only data such as a vector passed to sum |
Three passing modes for a function argument
Mistakes Beginners Make With References
Several misunderstandings come up repeatedly when students first use references. Each one is corrected by a rule stated above.
Common mistake Believing that assigning to a reference makes it refer to a different object
What it looks likeint x = 10; int& r = x; int y = 25; r = y;Why it failsA reference cannot be made to refer to a different object after its initialization. The assignment writes the value of y into x.
Do this insteadRead r = y as 'copy the value into the object r names', and expect x to change while r stays bound to x.
Common mistake Forgetting to initialize a reference
What it looks likeint& r;Why it failsThe initializer is not optional, because a reference without an object to stand for has no meaning.
Do this insteadDeclare the reference together with the object it names, such as int& r = x;Common mistake Using the star prefix to access a reference
Why it failsA pointer needs the star prefix to reach the referred value, but a reference is used by its name alone.
Do this insteadWrite r = 12 rather than attaching a star to r.
Common mistake Reading the leading type as the parameter type
What it looks likedouble sum(const vector<double>& v)Why it failsThe leading double is the return type. The parameter type is const vector<double>&.
Do this insteadRead the declaration from the inside out, starting at the name and moving outward through the declarator operators.
Common mistake Expecting a const reference to allow modification by a non-const method call
What it looks likev.push_back(1.0) inside a function whose parameter is const vector<double>&
Why it failsThe const promise forbids changes to the referred object through this name, so the modifying line is rejected.
Do this insteadKeep a const reference parameter read-only, or pass by non-const reference if the function must change the data.
Predicting Whether a Read-Only Function Compiles
What do you think happens?
Does double sum(const vector<double>& v) compile if its body calls v.push_back(1.0)?
Reveal answer
No, the call that modifies v is rejected at compile time.
The const on the parameter forbids changes to the referred vector through the name v. The failure is located on the push_back line, not in the loop or the return statement.
A function is declared as double average(const vector<double>& v) and its body first calls v.clear() and then sums the elements. Identify the line that fails, explain why it fails in terms of the const reference, and rewrite the body so it computes the average of the original elements without modifying v.
Hints
- Find the exact line that modifies the parameter, since the restriction is enforced there
- The const applies to the referred object, so any call that changes that object through v is forbidden
- A correct body can compute the sum and divide by
v.size()without changing v
Key Takeaways
- The unary suffix & in a declaration means reference to, and a reference is another name for an existing object that must be named at initialization.
- A reference is used by its plain name, has no star prefix, and cannot be redirected to a different object, so assignment writes into the object it names.
- A const reference lets a function read an argument without copying it while forbidding changes to the original through that name, and the rule is enforced at the modifying line.
- For a read-only function, choose by value for cheap data that should not be affected, and const reference for large data that should be read without copying.