Skip to content
C++
Language
since C++98
Basic

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++98

A 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

cpp
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.

WrittenResult
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.

cpp
#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)

cpp
#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)

cpp
#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 rvalue

std::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.

cpp
#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

cpp
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>.

cpp
#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 == 1

Best 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:

cpp
const int& danger(const int& x) { return x; }  // x may be a temporary
const int& r = danger(42);  // r is dangling immediately after the call

Forgetting std::move when calling a move constructor explicitly. A named rvalue-reference variable is itself an lvalue:

cpp
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 combinationCollapses 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:

  1. Template argument substitution. When a function template f(T&&) is called with an lvalue of type X, T deduces to X&. The parameter type then becomes X& &&, which collapses to X&. When called with an rvalue of type X, T deduces to X, and the parameter stays X&&.

  2. using and typedef aliases. If a type alias introduces a reference, and && is later appended during substitution, the same rules apply.

  3. decltype. A decltype expression that names an lvalue produces an lvalue reference type; when that type is later combined with &&, collapsing fires.

  4. std::remove_reference_t is 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:

cpp
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

cpp
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 && itself

The 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 typeBinds to lvalue?Binds to const lvalue?Binds to rvalue / temporary?Notes
T&yesnonoRequires a modifiable lvalue
const T&yesyesyesThe "universal acceptor" for read-only use
T&&nonoyesRvalues and xvalues only
const T&&noyesyesRare; disables moving while accepting temporaries
T&& (forwarding)yesyesyesIn a deduced context; collapses as above

Overload resolution and reference binding

When multiple overloads are viable, the compiler prefers an exact-category match:

cpp
#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:

cpp
void print(const std::string& s);
print("hello");  // const char* converts to std::string temporary; lifetime extended

A 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:

cpp
// 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:

cpp
{
    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 return statement
  • Through a struct member initializer (except in some constexpr contexts)
  • Through std::initializer_list construction
cpp
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 scope

Structured binding lifetimes

When a structured binding introduces names over a temporary, the anonymous backing object obeys the same extension rule:

cpp
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 extended

std::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.

cpp
#include <vector>
int a = 1, b = 2;
// std::vector<int&> v;  // ill-formed β€” references are not assignable

std::reference_wrapper interface

std::reference_wrapper<T> is a copyable, assignable wrapper that holds a pointer internally but exposes reference semantics:

cpp
#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 callable

Because 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:

cpp
#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 copy

Containers of references

cpp
#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 == 60

Use 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:

cpp
#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 == 2

Without 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:

cpp
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 b

Structured 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:

cpp
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 lvalue

Copy vs. reference β€” practical consequences

cpp
#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

cpp
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 Point

Structured bindings in if initializers (C++17)

cpp
if (auto [it, inserted] = map.insert({key, value}); inserted) {
    // inserted successfully; it is the iterator to the new element
}
// [it, inserted] do not exist here

C++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:

cpp
// 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:

cpp
#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:

  1. Const propagation: if T (after stripping references) is const-qualified, the result is const.
  2. Value-category propagation: if T is 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.

cpp
#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.

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 where
  • reference/language/const-correctness β€” how const interacts with reference parameters and member functions
  • reference/language/auto β€” auto&& deduction and forwarding references in generic lambdas (C++14) and range-for