References
A reference is an alias for an existing object; C++ provides lvalue references (C++98) and rvalue references (C++11) with distinct binding and lifetime rules.
Referencesince C++98A reference is an alias β an alternative name bound to an existing object β that cannot be reseated after initialization and that carries no storage of its own.
Overview
C++ has two reference categories introduced at different points in the standard:
Lvalue references (T&) arrived in C++98 and bind to named, addressable objects. Their primary roles are avoiding copies in function parameters and enabling in/out semantics.
Rvalue references (T&&) arrived in C++11 and bind to temporaries or objects explicitly cast to rvalue. They power move semantics and perfect forwarding.
A third surface, forwarding references (also called universal references), appears when T&& is written in a deduced context β either a function template parameter or auto&&. These collapse based on the value category of the argument, which is why they can bind to both lvalues and rvalues.
Syntax
int x = 42;
int& lref = x; // lvalue reference β binds to named object
const int& clref = 99; // const lvalue reference β extends lifetime of temporary (C++98)
int&& rref = 7 * 6; // rvalue reference β binds to temporary (C++11)
auto&& fwd = x; // forwarding reference β deduced as int& (C++11)
auto&& fwd2 = 42; // forwarding reference β deduced as int&& (C++11)Reference collapsing (C++11)
When references are layered through template substitution, the compiler applies collapsing rules. In practice: any combination involving an lvalue reference collapses to an lvalue reference; two rvalue references remain an rvalue reference.
| Written | Result |
|---|---|
T& & | T& |
T& && | T& |
T&& & | T& |
T&& && | T&& |
This is what makes std::forward work correctly.
Examples
Lvalue references as function parameters
Passing by const T& avoids a copy and remains valid for any argument that is implicitly convertible to T.
#include <string>
#include <iostream>
void print(const std::string& s) { // no copy, accepts temporaries
std::cout << s << '\n';
}
std::string& front(std::vector<std::string>& v) { // returns alias into v
return v.front();
}
int main() {
std::vector<std::string> names = {"alice", "bob"};
front(names) = "charlie"; // assigns through the reference
print(names.front()); // prints "charlie"
print("temporary"); // const& extends lifetime of the temporary
}Rvalue references and move semantics (C++11)
#include <vector>
#include <utility>
class Buffer {
std::vector<char> data_;
public:
explicit Buffer(std::size_t n) : data_(n) {}
// Move constructor β steals the resource instead of copying it
Buffer(Buffer&& other) noexcept : data_(std::move(other.data_)) {}
// Move assignment
Buffer& operator=(Buffer&& other) noexcept {
data_ = std::move(other.data_);
return *this;
}
};
Buffer make_buffer(std::size_t n) {
return Buffer{n}; // NRVO applies; if not, move constructor is called
}Perfect forwarding with forwarding references (C++11)
#include <utility>
#include <string>
template <typename T, typename Arg>
T make(Arg&& arg) { // Arg&& is a forwarding reference
return T(std::forward<Arg>(arg)); // preserves lvalue/rvalue category
}
std::string s = "hello";
auto s1 = make<std::string>(s); // copies: s is lvalue
auto s2 = make<std::string>(std::move(s)); // moves: cast to rvaluestd::forward<Arg>(arg) casts arg back to whatever category Arg was deduced as. Without it, named parameters are always lvalues inside the function body.
Lifetime extension via const lvalue reference (C++98)
A const T& bound directly to a temporary extends that temporary's lifetime to match the reference's scope.
#include <string>
const std::string& get() {
return "hello"; // DANGER: dangling β temporary destroyed at semicolon
}
void safe() {
const std::string& r = std::string("hello"); // OK: lifetime extended
// r is valid until the end of this scope
}This rule applies only when the reference is the direct binding site. It does not propagate through function returns.
Reference members in classes
class View {
const std::vector<int>& data_; // reference member β no ownership
public:
explicit View(const std::vector<int>& v) : data_(v) {}
int operator[](std::size_t i) const { return data_[i]; }
};Because reference members cannot be reseated, a class with a reference member is not assignable by default; the compiler deletes the copy-assignment operator.
std::ref and std::cref (C++11)
Standard algorithms and thread constructors copy their arguments. To pass a reference through such an interface, wrap it with std::ref or std::cref from <functional>.
#include <functional>
#include <thread>
void increment(int& n) { ++n; }
int count = 0;
std::thread t(increment, std::ref(count)); // passes reference, not copy
t.join();
// count == 1Best Practices
Prefer const T& for read-only parameters of non-trivial types to avoid copies without sacrificing call-site flexibility. For trivially copyable types smaller than a pointer, pass by value instead.
Use T&& in move constructors and move-assignment operators and mark them noexcept (C++11). The standard library moves elements into containers only when the operation is marked noexcept.
Use forwarding references only in templates. Writing T&& in a non-template context is always an rvalue reference, not a forwarding reference. The two look identical but behave differently.
Never return a local variable by reference. The object is destroyed when the function returns; the caller holds a dangling reference. Return by value and rely on NRVO or move semantics.
Avoid reference members in copyable types. They silently delete copy-assignment and complicate object composition. Consider a pointer or std::reference_wrapper if reseatability matters.
Common Pitfalls
Dangling references from temporaries. Assigning a reference-to-temporary to const T& inside a function parameter does not extend lifetime:
const int& danger(const int& x) { return x; } // x may be a temporary
const int& r = danger(42); // r is dangling immediately after the callForgetting std::move when calling a move constructor explicitly. A named rvalue-reference variable is itself an lvalue:
void process(Widget&& w) {
Widget copy = w; // copies β w is a named variable (lvalue)
Widget moved = std::move(w); // moves β correct
}Mistaking auto&& for "rvalue reference." auto&& is a forwarding reference; its deduced type depends on the initialiser's value category. Use it deliberately (range-for loops, generic lambdas) rather than as a shorthand for auto.
Reference collapsing surprises in templates. If a template parameter is substituted with a reference type and then another && is appended, collapse rules apply silently. This is intentional β it enables std::forward β but can be surprising when inspecting deduced types.
Reference Collapsing Rules In Depth (C++11)
Reference collapsing is the mechanism that makes forwarding references and std::forward possible. It is triggered any time the language would otherwise produce a "reference to a reference" β a construct that is illegal to write directly but can arise through template substitution or typedef/using aliases.
The four rules
| Substituted combination | Collapses to |
|---|---|
T& & | T& |
T& && | T& |
T&& & | T& |
T&& && | T&& |
The rule has a memorable summary: any lvalue reference in the mix produces an lvalue reference; only rvalue-plus-rvalue produces an rvalue reference. One lvalue reference "infects" the whole chain.
When collapsing is triggered
Collapsing applies in four contexts:
-
Template argument substitution. When a function template
f(T&&)is called with an lvalue of typeX,Tdeduces toX&. The parameter type then becomesX& &&, which collapses toX&. When called with an rvalue of typeX,Tdeduces toX, and the parameter staysX&&. -
usingandtypedefaliases. If a type alias introduces a reference, and&&is later appended during substitution, the same rules apply. -
decltype. Adecltypeexpression that names an lvalue produces an lvalue reference type; when that type is later combined with&&, collapsing fires. -
std::remove_reference_tis NOT involved in collapsing β that strips the reference entirely, which is the opposite of collapsing.
Why this enables std::forward
std::forward is typically implemented as:
template <typename T>
T&& forward(std::remove_reference_t<T>& arg) noexcept {
return static_cast<T&&>(arg);
}When T is deduced as Widget& (lvalue case), T&& becomes Widget& &&, which collapses to Widget& β a cast that preserves the lvalue. When T is Widget (rvalue case), T&& is Widget&& β a cast that restores the rvalue category. Reference collapsing does the heavy lifting; std::forward is just a wrapper around static_cast.
Forwarding references vs. rvalue references β a visual distinction
template <typename T>
void f(T&& x); // forwarding reference β T is deduced
void g(std::string&& s); // rvalue reference β no deduction
template <typename T>
void h(std::vector<T>&& v); // rvalue reference β T is deduced, but not for the && itselfThe third case is subtle: T is deduced from the element type, but the && on std::vector<T>&& is not in a directly deduced position for the reference qualifier, so h only accepts rvalues.
Reference Binding Hierarchy
Every reference type has a precise set of expression categories it will accept. Getting this wrong is a common source of compiler errors and subtle bugs.
Complete binding table
| Reference type | Binds to lvalue? | Binds to const lvalue? | Binds to rvalue / temporary? | Notes |
|---|---|---|---|---|
T& | yes | no | no | Requires a modifiable lvalue |
const T& | yes | yes | yes | The "universal acceptor" for read-only use |
T&& | no | no | yes | Rvalues and xvalues only |
const T&& | no | yes | yes | Rare; disables moving while accepting temporaries |
T&& (forwarding) | yes | yes | yes | In a deduced context; collapses as above |
Overload resolution and reference binding
When multiple overloads are viable, the compiler prefers an exact-category match:
#include <string>
void sink(std::string& s); // (1) lvalue overload
void sink(const std::string& s); // (2) const lvalue overload
void sink(std::string&& s); // (3) rvalue overload
std::string s = "hello";
const std::string cs = "world";
sink(s); // calls (1) β modifiable lvalue, exact match
sink(cs); // calls (2) β const lvalue, exact match
sink(std::move(s)); // calls (3) β rvalue, exact match
sink("temporary"); // calls (3) β prvalue converts to std::string&&An rvalue will bind to const T& if no T&& overload exists, but T&& always wins over const T& for rvalue arguments when both are present.
Implicit conversions and reference binding
const T& can bind to a temporary created by implicit conversion:
void print(const std::string& s);
print("hello"); // const char* converts to std::string temporary; lifetime extendedA non-const T& cannot, because binding a modifiable reference to a temporary was outlawed to prevent silent discarding of modifications.
Dangling References and Lifetime Extension
What causes a dangling reference
A reference becomes dangling when the object it names is destroyed while the reference still exists. The three most common causes:
// 1 β returning reference to local
int& bad_local() {
int x = 42;
return x; // x destroyed on return; caller holds dangling reference
}
// 2 β returning reference to temporary argument
const int& bad_param(const int& x) {
return x; // if caller passed a temporary, it's already gone
}
const int& r = bad_param(42); // dangling immediately
// 3 β reference member outliving its referent
struct View {
const std::string& ref;
};
View make_view() {
std::string s = "local";
return View{s}; // s is destroyed; View::ref is dangling
}Lifetime extension β what it covers
Binding a const T& or a T&& directly to a temporary extends the temporary's lifetime to match the reference's scope:
{
const std::string& r = std::string("hello"); // temporary's life extended
std::string&& rr = std::string("world"); // also extended
// both strings are valid until the closing brace
}This is a deliberate language rule (C++98 for const T&, C++11 for T&&). It enables idioms like range-for over temporaries.
The one-level limit β the critical exception
Lifetime extension applies only at the direct binding site. It does not propagate:
- Through a function parameter, even if that parameter is a
const T& - Through a
returnstatement - Through a struct member initializer (except in some constexpr contexts)
- Through
std::initializer_listconstruction
const std::string& leak(const std::string& s) {
return s; // s may alias a temporary; returning it does NOT extend lifetime
}
const std::string& r = leak(std::string("boom"));
// r is dangling β the temporary was destroyed when leak() returned
// Safe pattern: bind directly
const std::string& safe = std::string("ok"); // extended at THIS scopeStructured binding lifetimes
When a structured binding introduces names over a temporary, the anonymous backing object obeys the same extension rule:
auto [a, b] = std::pair<int, int>{1, 2}; // anonymous pair lives until end of block
const auto& [x, y] = std::make_pair(10, 20); // also extendedstd::reference_wrapper, std::ref, and std::cref (C++11)
Raw references have two fundamental restrictions: they cannot be stored in containers, and they cannot be reseated after initialization. std::reference_wrapper<T> (from <functional>) solves the first problem and relaxes the second.
Why raw references cannot go in containers
Standard containers require that their value type be CopyAssignable and CopyConstructible. References satisfy neither: copy-assigning a reference assigns through it (changing the referent), not the reference itself. The standard explicitly prohibits containers of references.
#include <vector>
int a = 1, b = 2;
// std::vector<int&> v; // ill-formed β references are not assignablestd::reference_wrapper interface
std::reference_wrapper<T> is a copyable, assignable wrapper that holds a pointer internally but exposes reference semantics:
#include <functional>
T& get() const noexcept; // access the wrapped reference
operator T&() const noexcept; // implicit conversion to T&
template <typename... Args>
invoke_result_t<T&, Args...>
operator()(Args&&...) const; // call-through if T is callableBecause it stores a pointer under the hood, it can be copied, assigned, and stored in containers while still behaving as a reference at the point of use.
std::ref and std::cref helpers
std::ref(x) returns std::reference_wrapper<T> wrapping x. std::cref(x) returns std::reference_wrapper<const T>. They are the idiomatic way to pass references through interfaces that copy their arguments:
#include <functional>
#include <algorithm>
#include <vector>
#include <numeric>
// Problem: std::for_each copies its function object
struct Accumulator {
int total = 0;
void operator()(int n) { total += n; }
};
Accumulator acc;
std::vector<int> v{1, 2, 3, 4, 5};
std::for_each(v.begin(), v.end(), std::ref(acc));
// acc.total == 15 β std::ref passes acc by reference, not by copyContainers of references
#include <functional>
#include <vector>
int x = 10, y = 20, z = 30;
std::vector<std::reference_wrapper<int>> refs = {x, y, z};
for (int& r : refs) { // implicit conversion to int&
r *= 2;
}
// x == 20, y == 40, z == 60Use with std::bind
std::bind copies all its arguments. To bind a callable by reference (so that state mutations are visible), wrap it with std::ref:
#include <functional>
struct Counter {
int n = 0;
void tick() { ++n; }
};
Counter c;
auto bound = std::bind(&Counter::tick, std::ref(c));
bound(); // c.n == 1
bound(); // c.n == 2Without std::ref(c), std::bind would capture a copy of c, and the original would never be incremented.
Reseatability
Unlike a raw reference, a std::reference_wrapper can be "reseated" by copy-assigning a new wrapper to it:
int a = 1, b = 2;
auto rw = std::ref(a); // wraps a
rw = std::ref(b); // now wraps b β the reference was reseated
int& r = rw; // r refers to bStructured Bindings and References (C++17)
Structured bindings introduce names bound to the elements of an aggregate, array, or tuple-like type. The reference semantics depend entirely on the cv-ref qualifier on auto.
The hidden anonymous object
Every structured binding declaration creates a hidden anonymous entity e that is initialized from the right-hand-side expression. The declared names [a, b, ...] are bound to elements of e β they are not independent objects. This model explains all the surprising behaviors:
auto [a, b] = expr; // e is a copy of expr; a and b alias elements of e
auto& [a, b] = expr; // e is a reference to expr; a and b alias expr's elements
const auto& [a, b] = expr; // e is a const reference; temporary lifetime extended
auto&& [a, b] = expr; // e is a forwarding ref; lvalue ref if expr is lvalueCopy vs. reference β practical consequences
#include <map>
#include <string>
std::map<std::string, int> scores{{"Alice", 95}, {"Bob", 87}};
// (1) copy β mutation does not affect the map
for (auto [name, score] : scores) {
score += 10; // changes the copy inside the loop body only
}
// (2) reference β mutation IS visible in the map
for (auto& [name, score] : scores) {
score += 10; // modifies the map in-place
}
// (3) const reference β read-only, no copy overhead
for (const auto& [name, score] : scores) {
// score += 10; // error: score is const
}When the binding is itself a reference
The individual names introduced by a structured binding are not quite ordinary variables. Under the tuple-like protocol, each name is defined as auto& a = get<0>(e); (or a const version). For aggregates, the binding names alias the members of e directly. Either way, the binding names are references to elements of the anonymous object e.
Consequence: you cannot take the address of a structured binding name with & if the underlying protocol uses get<> and returns a temporary β but for aggregates and arrays this is always safe.
const-ness propagates through the anonymous object
struct Point { int x, y; };
const Point p{1, 2};
auto [a, b] = p; // a and b are int (copied from const Point)
const auto& [c, d] = p; // c and d are const int& into p
auto& [e, f] = p; // ill-formed: cannot bind non-const ref to const PointStructured bindings in if initializers (C++17)
if (auto [it, inserted] = map.insert({key, value}); inserted) {
// inserted successfully; it is the iterator to the new element
}
// [it, inserted] do not exist hereC++23: std::forward_like
The problem with member forwarding
When writing a generic wrapper, you often want to forward a member of *this with the same value category as *this. Before C++23, this required intricate conditional logic or four overloads:
// C++20 approach β four overloads needed to handle all combinations
template <typename T>
struct Wrapper {
T value;
T& get() & { return value; }
const T& get() const& { return value; }
T&& get() && { return std::move(value); }
const T&& get() const&&{ return std::move(value); }
};With deducing this (C++23, P0847) and std::forward_like, all four collapse into one:
#include <utility> // std::forward_like
template <typename T>
struct Wrapper {
T value;
template <typename Self>
auto&& get(this Self&& self) {
return std::forward_like<Self>(self.value);
// if Self is Wrapper&, returns T&
// if Self is const Wrapper&, returns const T&
// if Self is Wrapper&&, returns T&&
// if Self is const Wrapper&&,returns const T&&
}
};How std::forward_like computes the return type
std::forward_like<T>(x) applies two transformations from the "owner" type T to the "member" expression x:
- Const propagation: if
T(after stripping references) is const-qualified, the result is const. - Value-category propagation: if
Tis an rvalue reference (or not a reference at all), the result is an rvalue reference; otherwise it is an lvalue reference.
The const comes from the owner (T), not from the member itself. This is precisely what is needed: a non-const wrapper holding a non-const member accessed through an rvalue should yield an rvalue member β enabling moves out of temporaries.
#include <utility>
#include <type_traits>
int x = 42;
// Owner is int& (lvalue ref, non-const) β member stays lvalue, non-const
static_assert(std::is_same_v<
decltype(std::forward_like<int&>(x)), int&>);
// Owner is const int& β member becomes const lvalue
static_assert(std::is_same_v<
decltype(std::forward_like<const int&>(x)), const int&>);
// Owner is int&& β member becomes rvalue ref
static_assert(std::is_same_v<
decltype(std::forward_like<int&&>(x)), int&&>);
// Owner is const int&& β member becomes const rvalue ref
static_assert(std::is_same_v<
decltype(std::forward_like<const int&&>(x)), const int&&>);Contrast with std::forward
std::forward<T>(x) propagates the value category of T onto x, but preserves x's own const-ness. std::forward_like<T>(x) propagates both the value category and the const-ness of T onto x. Use std::forward_like when x is a subobject that should "inherit" the owner's qualifications.
Header
std::forward_like lives in <utility> and is available from C++23 (since: 23).
See Also
reference/language/value-categoriesβ lvalue, xvalue, and prvalue classifications that govern which references can bind wherereference/language/const-correctnessβ howconstinteracts with reference parameters and member functionsreference/language/autoβauto&&deduction and forwarding references in generic lambdas (C++14) and range-for