An lvalue reference can only bind to a modifiable object. Therefore, the code excerpt below throws a compiler error.
int main()
{
const int x { 5 }; // x is const
int& ref { x }; // error: attempt to reference a const object
}
The workaround is to simply make the lvalue reference const.
Lvalue reference to const
By labelling the lvalue reference as const we tell it to treat the referenced type as const. This is called a const reference.
Const references cannot modify the referenced type.
#include <iostream>
int main()
{
const int maxSpeed { 120 };
const int& ref { maxSpeed };
std::cout << ref << '\\n';
}
// Output
120
Lvalue reference to const have some unique properties that are worth discussing.
Binding a const reference to a non-const object
A const lvalue reference can also bind to a non-const object.
This is useful when you want to read an object through an alias but prevent modification through that alias.
#include <iostream>
int main(){
int batteryPercent { 75 };
const int& readOnlyBattery { batteryPercent };
std::cout << "Initial: " << readOnlyBattery << "%\\n";
batteryPercent = 82;
std::cout << "After direct update: " << readOnlyBattery << "%\\n";
// readOnlyBattery = 90; // error: cannot modify through const int&
}
// Output:
Initial: 75%
After direct update: 82%
The original object is not const:
int batteryPercent { 75 };
So it can still be modified directly:
batteryPercent = 82;
But it cannot be modified through the const reference:
readOnlyBattery = 90; // error
This is the key rule:
A
const T&does not make the original object const. It only prevents modification through that reference.
That is why const T& is common in function parameters. The function can read the caller’s object without copying it and without being allowed to modify it.
#include <iostream>
#include <string>
void printName(const std::string& name){
std::cout << "Name: " << name << '\\n';
// name += "!"; // error: cannot modify through const std::string&
}
int main(){
std::string user { "Ada" };
printName(user);
}
// Output:
Name: Ada
The function receives access to the original std::string, but only as a read-only reference.
const references can bind to rvalues
This is permissible, thereby extending the lifespan of the temporary object.
C++ creates a temporary int object from the literal 5, then binds ref to that temporary.
#include <iostream>
int main()
{
const int& ref { 5 }; // 5 is an rvalue
std::cout << ref << '\\n'; // prints 5
}
Binding to a different type
A const lvalue reference can also bind to a value of a different but convertible type.
#include <iostream>
int main(){
char letter { 'e' };
const int& code { letter };
std::cout << code << '\\n';
letter = 'z';
std::cout << code << '\\n';
}
// Output:
101
101
The character 'e' has an integer encoding value of 101 on common character sets such as ASCII-compatible systems.
However, code is not a reference to letter.
The reference type is:
const int&
but letter has type:
char
So C++ creates a temporary int, initializes it from letter, and binds code to that temporary.
That is why changing letter later does not change code:
letter = 'z';
The reference is bound to the temporary int, not to the original char.
Why const T& is common in function parameters
It’s because it’s compatible with lvalues and rvalues. Take a look at the program below:
#include <iostream>
#include <string>
void printReport(const std::string& report){
std::cout << "Report: " << report << '\\n';
}
int main(){
std::string message { "Motor current within limits" };
printReport(message);
printReport("Temporary report");
}
// Output:
Motor current within limits
Temporary report
When the compiler sees printReport("Temporary report"); it creates a temporary std::string so the reference can bind to it.
This provides a great deal of flexibility.
Summary
T&can only bind to a modifiable lvalue.const T&can bind to const objects, non-const objects, and rvalues.- A
const T&does not necessarily mean the original object is const; it only means the object cannot be modified through that reference.