Ready to practice?
Sign up to access interactive coding exercises and track your progress.
Read-Only References with const
Bind references to rvalues and prevent modification of referenced objects.
Lvalue References to Const
In the previous lesson, we learned that lvalue references can only bind to modifiable lvalues. This restriction prevents:
int main()
{
const int maxHealth{100}; // maxHealth is non-modifiable (const) lvalue
int& invalid{maxHealth}; // Error: reference can't bind to non-modifiable lvalue
return 0;
}
This prohibition makes sense: it would let us modify a const variable (maxHealth) through the non-const reference (invalid).
But what if we want to reference a const variable? Normal lvalue references won't work.
Lvalue References to Const
By adding const when declaring an lvalue reference, we tell the reference to treat its referent as const. This creates an lvalue reference to const (also called a reference to const or const reference).
Lvalue references to const can bind to non-modifiable lvalues:
int main()
{
const int maxHealth{100}; // Non-modifiable lvalue
const int& healthCap{maxHealth}; // Valid: lvalue reference to const
return 0;
}
Lvalue references to const treat their referents as const, allowing access but not modification:
#include <iostream>
int main()
{
const int maxHealth{100};
const int& healthCap{maxHealth};
std::cout << healthCap << '\n'; // Valid: read access
healthCap = 200; // Error: can't modify through const reference
return 0;
}
Binding to Modifiable Lvalues
Lvalue references to const can also bind to modifiable lvalues. The referent is treated as const when accessed through the reference (even though the underlying object is non-const):
#include <iostream>
int main()
{
int health{100}; // Modifiable lvalue
const int& readOnly{health}; // Valid: const reference to modifiable lvalue
std::cout << readOnly << '\n'; // Valid: read access
readOnly = 75; // Error: can't modify through const reference
health = 75; // Valid: health is modifiable directly
return 0;
}
In this program, const reference readOnly binds to modifiable lvalue health. We can access health through readOnly, but cannot modify it via readOnly. However, we can still modify health directly.
Prefer lvalue references to const over lvalue references to non-const unless you need to modify the referent.
Binding to Rvalues
Surprisingly, lvalue references to const can bind to rvalues:
#include <iostream>
int main()
{
const int& score{42}; // Valid: 42 is an rvalue
std::cout << score << '\n'; // Prints 42
return 0;
}
When this occurs, a temporary object is created and initialized with the rvalue, and the const reference binds to that temporary.
We covered temporary objects in the lesson on local scope.
Binding to Different Types
Lvalue references to const can bind to values of different types, provided implicit conversion exists:
#include <iostream>
int main()
{
// Case 1
const double& percentage{10}; // Temporary double initialized with 10, reference binds to it
std::cout << percentage << '\n'; // Prints 10
// Case 2
char grade{'A'};
const int& numericGrade{grade}; // Temporary int initialized with 'A', reference binds to it
std::cout << numericGrade << '\n'; // Prints 65 (ASCII value of 'A')
return 0;
}
In case 1, a temporary double object is created with value 10, then const double& percentage binds to it.
In case 2, a temporary int is created with the value of character 'A', then const int& numericGrade binds to it.
When binding a const lvalue reference to a different type, the compiler creates a temporary of the reference's type, initializes it with the value, then binds the reference to the temporary.
Note that numericGrade prints as int, not char, because numericGrade references a temporary int, not the original char grade.
We normally assume references are identical to their referents, but this breaks when references bind to temporaries (from copies or conversions). Modifications to the original object won't affect the reference (which references a different object), and vice versa.
Here's an example:
#include <iostream>
int main()
{
short items{10}; // Note: type is short
const int& inventory{items}; // Note: type is int&
--items; // Items decremented
if (inventory) // Inventory still non-zero?
{
std::cout << "Still have inventory!\n";
}
return 0;
}
Here, items is short and inventory is const int&. Since inventory can only bind to an int, the compiler implicitly converts items to int, creating a temporary int with value 10. The reference inventory binds to this temporary, not to items.
When items decrements, inventory remains unaffected (referencing a different object). Although we expect the if condition to evaluate false, it actually evaluates true.
Const References Extend Temporary Lifetimes
Temporaries normally destruct at the end of the expression creating them.
Given const int& healthCap{100};, if the temporary holding 100 were destroyed after initializing healthCap, the reference would dangle, causing undefined behavior upon access.
To prevent this, C++ has a special rule: When a const lvalue reference directly binds to a temporary, the temporary's lifetime extends to match the reference's lifetime.
#include <iostream>
int main()
{
const int& healthCap{100}; // Temporary holding 100 lives as long as healthCap
std::cout << healthCap << '\n'; // Safe to use
return 0;
} // Both healthCap and the temporary destruct here
When healthCap initializes with rvalue 100, a temporary is created and healthCap binds to it. The temporary's lifetime matches healthCap's lifetime, so we can safely use healthCap. Both destruct at block's end.
Lvalue references can only bind to modifiable lvalues.
Lvalue references to const can bind to modifiable lvalues, non-modifiable lvalues, and rvalues. This makes them much more flexible.
Advanced note: Lifetime extension only works with direct binding to temporaries. Temporaries returned from functions (even by const reference) don't qualify for lifetime extension. We show an example in the lesson on return by reference and return by address. For class types, binding a reference to a member extends the entire object's lifetime.
Why does C++ allow const references to bind to rvalues? We'll answer that in the next lesson!
Constexpr Lvalue References (Optional)
When applied to references, constexpr enables use in constant expressions. Constexpr references have a limitation: they can only bind to objects with static duration (globals or static locals). The compiler knows where static objects instantiate in memory, treating that address as a compile-time constant.
Constexpr references cannot bind to non-static local variables, whose addresses aren't known until function calls occur.
int globalHealth{100};
int main()
{
[[maybe_unused]] constexpr int& ref1{globalHealth}; // Valid: can bind to global
static int staticHealth{75};
[[maybe_unused]] constexpr int& ref2{staticHealth}; // Valid: can bind to static local
int localHealth{50};
[[maybe_unused]] constexpr int& ref3{localHealth}; // Error: can't bind to non-static
return 0;
}
When defining constexpr references to const variables, apply both constexpr (to the reference) and const (to the referenced type):
int main()
{
static const int maxHealth{100}; // A const int
[[maybe_unused]] constexpr const int& ref{maxHealth}; // Needs both constexpr and const
return 0;
}
Given these limitations, constexpr references see limited use.
Summary
Lvalue references to const: By adding const to an lvalue reference declaration, we create an lvalue reference to const (or const reference), which treats the referent as const and can bind to modifiable lvalues, non-modifiable lvalues, and rvalues.
Binding to modifiable lvalues: Lvalue references to const can bind to modifiable lvalues, treating them as const through the reference even though the underlying object remains modifiable directly.
Binding to rvalues: Lvalue references to const can bind to rvalues by creating a temporary object that the reference binds to, enabling passing of temporaries to functions.
Binding to different types: When binding to different types via implicit conversion, a temporary of the reference's type is created and the reference binds to that temporary, not the original object.
Lifetime extension: When a const lvalue reference directly binds to a temporary, the temporary's lifetime extends to match the reference's lifetime, preventing dangling references.
Constexpr references: Constexpr references can only bind to objects with static duration (globals or static locals), limiting their use.
Lvalue references to const provide significantly more flexibility than non-const lvalue references while maintaining safety through const-correctness. They form the foundation for efficient pass-by-reference patterns in modern C++.
Read-Only References with const - Quiz
Test your understanding of the lesson.
Practice Exercises
Lvalue References to Const
Explore the flexibility of const lvalue references. Learn how they can bind to modifiable lvalues, non-modifiable lvalues, and rvalues, and understand lifetime extension.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!