In the previous lesson (8.3 -- L-value references), we discussed how an l-value reference can only bind to a modifiable l-value. This means the following is illegal:
1 2 3 4 5 6 7 |
int main() { const int x { 5 }; // x is a non-modifiable (const) l-value int& ref { x }; // error: ref can not bind to non-modifiable l-value return 0; } |
But what if we want to have a reference to a non-modifiable l-value?
L-value reference to a const value
We can tell an l-value reference to treat the object it is referencing as const by using the const
keyword when declaring the reference. This makes the l-value reference an l-value reference to a const value (sometimes called a reference to const or a const reference).
L-value references to const
can bind to non-modifiable l-values:
1 2 3 4 5 6 7 |
int main() { const int x { 5 }; // x is a non-modifiable l-value const int& ref { x }; // okay: ref is a an l-value reference to a const value return 0; } |
Because l-value references to const
treat their values as const, they can be used to access but not modify the value being referenced:
1 2 3 4 5 6 7 8 9 10 11 12 |
#include <iostream> int main() { const int x { 5 }; // x is a non-modifiable l-value const int& ref { x }; // okay: ref is a an l-value reference to a const value std::cout << ref; // okay: prints 5 ref = 6; // error: can not modify const value return 0; } |
Initializing an l-value reference to const with a modifiable l-value
L-value references to const
can also bind to modifiable l-values:
1 2 3 4 5 6 7 8 9 10 |
int main() { int x { 5 }; // x is a modifiable l-value const int& ref { x }; // okay x = 6; // okay: x is a modifiable l-value ref = 7; // error: ref is const! return 0; } |
When a lvalue reference to a const value
binds to a modifiable l-value, the value is treated as const when accessed through the reference, even though the underlying value is non-const. For this reason (as shown in the program above), we can modify the value of x
through variable x
(which is non-const), but not through ref
(which treats the value as const).
Rule
Favor l-value references to const
over l-value references to non-const
unless you need to modify the object being referenced.
Initializing an l-value reference to const with an r-value
Perhaps surprisingly, l-values references to const
can also bind to r-values.
1 2 3 4 5 6 7 8 9 10 |
#include <iostream> int main() { const int& ref { 5 }; // okay: 5 is an r-value std::cout << ref; // prints 5 return 0; } |
When this happens, a temporary object is created and initialized with the r-value, and the reference is bound to that temporary object.
A temporary object (also sometimes called an unnamed object) is an object that has no name. Temporary objects have no scope at all (this makes sense, since scope is a property of an identifier, and temporary objects have no identifier). This means a temporary object can usually only be used at the point where it is created, since there is no way to refer to it beyond that point.
References to r-values extend the lifetime of the referenced value
Normally, temporary objects are destroyed at the end of the expression in which they are created.
Consider what would happen in the above example if the temporary object created to hold r-value 5
was destroyed at the end of the expression that initializes ref
. Reference ref
would be left dangling (referencing an object that had been destroyed), and we’d get undefined behavior when we tried to access the value of ref
.
To avoid dangling references in such cases, C++ has a special rule: When a reference to a const value
is initialized with an r-value, the lifetime of the temporary object created is extended to match the lifetime of the reference. This is the exception to the rule that references and referents have independent lifetimes.
1 2 3 4 5 6 7 8 |
#include <iostream> int main() { const int& ref { 5 }; // The temporary object holding 5 has its lifetime extended to match ref std::cout << ref; // Therefore, we can safely use it here } // Both ref and the temporary object die here |
In the above example, when ref
is initialized with r-value 5
, a temporary object is created and ref
is bound to that temporary object. The lifetime of the temporary object matches the lifetime of ref
. Thus, we can safely print the value of ref
in the next statement. Then both ref
and the temporary object die at the end of the block.
Key insight
L-value references can only bind to modifiable l-values.
L-value references to const can bind to both modifiable and non-modifiable l-values, as well as r-values. This makes them a much more flexible type of reference.
So why does C++ allow an l-value references to const
to bind to an r-value anyway? We’ll answer that question in the next lesson.
![]() |
![]() |
![]() |
Leave a Reply