References: Lvalue, Rvalue & Pitfalls
Explore lvalue and rvalue references, understand C++ value categories, learn reference collapsing rules and forwarding references, and avoid dangling reference bugs.
Lvalue References
An lvalue reference (T&) is an alias — another name for an existing object. Unlike pointers, references must be initialized, cannot be null, and cannot be re-seated to refer to a different object.
Lvalue references bind to lvalues — expressions that have identity (you can take their address). Variables, array elements, and dereferenced pointers are all lvalues. A non-const lvalue reference (int&) cannot bind to a temporary (rvalue) because modifying a temporary is almost always a bug.
Lvalue Reference Basics
Lvalue references are the workhorse of C++ — used for pass-by-reference, operator overloading, and avoiding copies.
#include <iostream>
#include <string>
void append_greeting(std::string& s) {
s += ", welcome!"; // modifies the caller's string
}
int main() {
int x = 10;
int& ref = x; // ref is an alias for x
ref = 20;
std::cout << x << '\n'; // 20 — x was modified through ref
// const reference can bind to a temporary (rvalue)
const std::string& greeting = std::string("Hello");
// The temporary's lifetime is extended to match greeting's scope
std::cout << greeting << '\n'; // "Hello"
// Non-const lvalue reference CANNOT bind to rvalue:
// std::string& bad = std::string("oops"); // COMPILER ERROR
std::string name = "Alice";
append_greeting(name);
std::cout << name << '\n'; // "Alice, welcome!"
}Value Categories in C++
C++ has a precise taxonomy of expression value categories. Understanding them is essential for move semantics and perfect forwarding:
- lvalue — has identity, cannot be moved from implicitly. Examples: variables, *ptr, arr[i], string literals.
- prvalue (pure rvalue) — has no identity, can be moved from. Examples: 42, x + y, std::string("temp"), function returning by value.
- xvalue (expiring value) — has identity AND can be moved from. Examples: std::move(x), static_cast, member of an rvalue.
- glvalue (generalized lvalue) — lvalue or xvalue (has identity).
- rvalue — prvalue or xvalue (can be moved from).
The key insight: an rvalue reference variable is itself an lvalue — it has a name and an address. This is the source of much confusion and is precisely why std::forward exists.
Rvalue References (T&&)
Rvalue references bind to rvalues (temporaries and std::move'd objects). They are the foundation of move semantics — they let you detect when an object is about to be destroyed and steal its resources.
#include <iostream>
#include <string>
#include <utility> // std::move
void process(const std::string& s) {
std::cout << "lvalue: " << s << '\n';
}
void process(std::string&& s) {
std::cout << "rvalue: " << s << '\n';
// s is an lvalue inside this function (it has a name!)
// To move from s, you'd need std::move(s)
}
int main() {
std::string a = "hello";
process(a); // calls lvalue overload
process(std::string("world")); // calls rvalue overload
process(std::move(a)); // calls rvalue overload — a is now moved-from
// IMPORTANT: a is in a valid but unspecified state
// Only safe operations: assign to it, destroy it
std::cout << "a after move: '" << a << "'\n"; // likely empty
}Reference Collapsing & Forwarding References
When references combine in templates, reference collapsing rules apply:
- T& & collapses to T&
- T& && collapses to T&
- T&& & collapses to T&
- T&& && collapses to T&&
The rule is simple: if either reference is an lvalue reference, the result is an lvalue reference. Only && && gives &&.
A forwarding reference (also called universal reference) is T&& where T is a deduced template parameter. It can bind to both lvalues and rvalues. auto&& is also a forwarding reference. But std::vector is NOT — T is already known. const T&& is also not a forwarding reference.
A dangling reference refers to a destroyed object. Unlike dangling pointers, there is no nullptr check — the code will silently use garbage data or crash.
Common causes:
- Returning a reference to a local variable — the variable is destroyed when the function returns.
- Capturing by reference in a lambda that outlives the referenced variable.
- const auto& to a temporary inside an expression — const auto& val = get_map()["key"] is dangling if get_map() returns by value, because the temporary map is destroyed at the semicolon.
- Range-based for loop over a temporary — for (auto& x : get_vector()) is fine (temporary lives for the loop), but for (auto& x : get_wrapper().get_vector()) can dangle if the wrapper is destroyed.
Lifetime Extension with const T&
A const lvalue reference can extend the lifetime of a temporary, but only when directly bound. This does NOT work through function calls or member access chains.
#include <iostream>
#include <string>
std::string make_greeting() { return "Hello, World!"; }
const std::string& identity(const std::string& s) { return s; }
int main() {
// SAFE: const ref directly bound to temporary
const std::string& good = make_greeting();
std::cout << good << '\n'; // OK — lifetime extended
// DANGEROUS: lifetime extension does NOT propagate through functions
const std::string& bad = identity(make_greeting());
// bad is DANGLING — the temporary was destroyed after identity() returned
// std::cout << bad << '\n'; // UNDEFINED BEHAVIOR
// SAFE: just use a value
std::string safe = make_greeting(); // move or copy-elision
std::cout << safe << '\n'; // always safe
}- Lvalue references (
T&) are aliases — must be initialized, cannot be null or re-seated const T&can bind to temporaries and extends their lifetime (but only when directly bound)- Rvalue references (
T&&) bind to temporaries and enable move semantics - A named rvalue reference is itself an lvalue — this is critical to understand
- Reference collapsing: any
&in the chain makes the result& - Forwarding references (
T&&with deducedT) bind to both lvalues and rvalues
Quiz — Test Your Knowledge
(15 XP)1. Given `void f(std::string&& s)`, what is the value category of `s` inside `f`?
2. Which reference collapsing rule is correct?
3. When does `const T&` lifetime extension work for temporaries?