Declaring a named variable as a reference means declaring an alias for an existing object or function.
References can be regarded as C++-encapsulated non-null pointers. They can be used to pass the object they point to, and must point to an object when declared.
A reference is not an object, so there are no arrays of references, a pointer to a reference cannot be obtained, and references to references do not exist.
Reference types are not object types
If you want a reference to support ordinary operations such as copying and assignment, for example as a container element, you need reference_wrapper, which is usually implemented by maintaining a non-null pointer.
References are mainly divided into two types: lvalue references and rvalue references.
Lvalues and rvalues
For an explanation of lvalues and rvalues, please refer to the value categories page.
Lvalue References T&
The references we usually encounter are lvalue references, that is, references bound to lvalues. At the same time, an lvalue reference qualified with const can bind to an rvalue. The following is an example from the reference manual.
#include <iostream>
#include <string>
int main() {
std::string s = "Ex";
std::string& r1 = s;
const std::string& r2 = s;
r1 += "ample"; // Modifying r1 modifies s
// r2 += "!"; // Error: cannot modify through a reference to const
std::cout << r2 << '\n'; // Print r2, access s, and output "Example"
}
The most common use of lvalue references is as function parameters, to avoid unnecessary copies.
#include <iostream>
#include <string>
// s in the parameter list is a reference, so no copy occurs when the function is called
char& char_number(std::string& s, std::size_t n) {
s += s; // 's' and 'str' in main() are the same object
// This also shows that an lvalue can appear on the right side of an equals sign
return s.at(n); // string::at() returns a reference to char
}
int main() {
std::string str = "Test";
char_number(str, 1) = 'a'; // The function returns an lvalue, which can be assigned to
std::cout << str << '\n'; // This outputs "TastTest"
}
Rvalue References T&& (C++ 11)
An rvalue reference is a reference bound to an rvalue. It is used to move objects and can also extend the lifetime of a temporary object.
#include <iostream>
#include <string>
using namespace std;
int main() {
string s1 = "Test";
// string&& r1 = s1; // Error: cannot bind to an lvalue; std::move or static_cast is needed
const string& r2 = s1 + s1; // Valid: an lvalue reference to const extends the lifetime
// r2 += "Test"; // Error: cannot modify through a reference to const
cout << r2 << '\n';
string&& r3 = s1 + s1; // Valid: an rvalue reference extends the lifetime
r3 += "Test";
cout << r3 << '\n';
const string& r4 = r3; // An rvalue reference can be converted to a const-qualified lvalue
cout << r4 << '\n';
string& r5 = r3; // An rvalue reference can be converted to an lvalue
cout << r5 << '\n';
}
Dangling References
When the object referred to by a reference has already been destroyed, the reference becomes a dangling reference. Accessing a dangling reference is undefined behavior and may cause the program to crash.
The following are common examples of dangling references:
-
Reference to a local variable
#include <iostream> int& foo() { int a = 1; return a; } int main() { int& b = foo(); std::cout << b << std::endl; // Undefined behavior } -
Dangling reference caused by deallocation
#include <iostream> int main() { int* ptr = new int(10); int& ref = *ptr; delete ptr; std::cout << ref << std::endl; // Undefined behavior } -
Dangling reference caused by memory reallocation
#include <iostream> int main() { std::string str = "hello"; const char& ref = str.front(); str.append("world"); // May reallocate memory, causing the memory referred to by ref to be released std::cout << ref << std::endl; // Undefined behavior }Insertion operations on containers such as
std::vectorandstd::unordered_mapmay also cause memory reallocation.
When using references, always pay attention to the lifetime of the object to which the reference points, and avoid creating dangling references.
Static analysis tools and good coding habits can usually help us avoid dangling-reference problems.
Reference-Related Optimization Techniques
Eliminate the Copying Cost of Passing Non-Lightweight Objects as Arguments
Common non-lightweight objects include:
- Containers such as
vector,array, andmap string- Types that implement or inherit special functions such as custom copy constructors and move constructors
Using references for lightweight objects, however, provides no benefit. The space occupied by a reference type used as a parameter may even be larger than that occupied by the type itself.
This may introduce some performance overhead and may also prevent compiler optimizations.
The following are lightweight objects:
- Fundamental types such as
intandfloat - Small aggregate types
- Iterators of standard-library containers
Convert an Lvalue to an Rvalue
Use std::move to transfer ownership of an object. This is commonly seen between local variables, or between a parameter and a local variable:
#include <iostream>
#include <string>
#include <vector>
using namespace std;
string world(string str) { return std::move(str) += " world!"; }
int main() {
// 1
cout << world("hello") << '\n';
vector<string> vec0;
// 2
{
string&& size = to_string(vec0.size());
size += ", " + to_string(size.size());
vec0.emplace_back(std::move(size));
}
cout << vec0.front();
}
However, this is not necessary all the time, for example in copy elision.
Extend the Lifetime of a Temporary with an Rvalue
Semantically, a temporary may incur additional copying or moving. Although in most cases the compiler can optimize this through copy elision, a reference can force the compiler not to perform these redundant operations, avoiding uncertainty.