References: Aliases with Semantics
References are aliases to existing objects, unlike pointers which are objects themselves. C++ provides two kinds: lvalue references (T&) and rvalue references (T&&, C++11).
int x = 10;
int& ref = x; // Lvalue reference to x
ref = 20; // Modifies x
int* ptr = &x; // Pointer is a separate object
*ptr = 30; // Modifies x through pointer
Key Properties of References
- Must be initialized: Unlike pointers, references cannot be null or uninitialized
- Cannot be rebound: Once bound, a reference always refers to the same object
- No separate storage: References typically compile to pointers but have different semantics
- No reference to reference:
int& &is illegal (but see reference collapsing)
int a = 5;
int& r1 = a;
// int& r2; // Error: must be initialized
// r1 = b; // This assigns b's value to a, doesn't rebind r1
Value Categories: lvalues and rvalues
Every C++ expression belongs to a value category. The fundamental distinction:
- lvalue: Has persistent identity (name, memory address)
- rvalue: Temporary, about to be destroyed
int x = 10; // x is an lvalue
int* p = &x; // OK: can take address of lvalue
// int* p = &10; // Error: can't take address of rvalue
// int& r = 42; // Error: lvalue ref can't bind to rvalue
C++11 introduced finer categories:
Rvalue References and Move Semantics
Rvalue references (T&&) bind to temporaries, enabling move semantics: transferring resources instead of copying.
std::string s1 = "hello";
std::string s2 = s1; // Copy: s1 still valid
std::string s3 = std::move(s1); // Move: s1 left in valid but unspecified state
void process(std::string&& s) {
// s is an rvalue reference (but s itself is an lvalue!)
}
process(std::string("temp")); // OK: binds to temporary
process(std::move(s2)); // OK: std::move casts to rvalue
// process(s2); // Error: lvalue can't bind to &&
The lvalue-rvalue Paradox
Inside a function, an rvalue reference parameter is itself an lvalue (it has a name):
void func(std::string&& s) {
std::string copy = s; // Copy, not move (s is lvalue)
std::string moved = std::move(s); // Move (explicitly cast to rvalue)
}
Const lvalue References: Universal Binding
A const T& can bind to both lvalues and rvalues, making it perfect for read-only parameters:
void print(const std::string& s) { // Accepts anything
std::cout << s;
}
std::string str = "hello";
print(str); // OK: binds to lvalue
print("world"); // OK: binds to temporary
print(str + " world"); // OK: binds to temporary result
This is why pre-C++11 code used const T& everywhere: it’s the only reference type that accepts both lvalues and rvalues.
Reference Collapsing Rules
When references are formed through templates or typedef, C++ applies reference collapsing:
T& & → T&
T& && → T&
T&& & → T&
T&& && → T&&
Rule: An rvalue reference to an rvalue reference becomes rvalue reference; everything else becomes lvalue reference.
template<typename T>
void wrapper(T&& arg) { // Forwarding reference (see next section)
// If T is int&, then T&& becomes int& (reference collapsing)
// If T is int, then T&& is int&&
}
Forwarding References (Universal References)
In template contexts, T&& has special meaning—it’s a forwarding reference that can bind to anything:
template<typename T>
void forward_wrapper(T&& arg) { // Forwarding reference
target(std::forward<T>(arg)); // Perfect forwarding
}
int x = 10;
forward_wrapper(x); // T = int&, arg is int&
forward_wrapper(10); // T = int, arg is int&&
std::forward<T> preserves the value category: forwards lvalues as lvalues, rvalues as rvalues.
Reference Semantics
int x = 5; int& r = x; r = 10; std::cout << ; // Outputs 10
Interactive Lab
Waiting for signal...