Lifetime of Temporaries Bound to References in C++

In C++, temporary objects are normally destroyed at the end of the full-expression in which they are created. However, binding a temporary object to a reference can extend its lifetime to match that of the reference, preventing dangling references. This article explains the core rules governing temporary lifetime extension, how it applies to const lvalue and rvalue references, and the crucial exceptions where this extension fails.

The Basic Rule of Lifetime Extension

According to the C++ standard, when a temporary object is bound to a const lvalue reference (const T&) or an rvalue reference (T&&), its lifetime is extended to match the lifetime of that reference.

Under normal circumstances, a temporary is destroyed when the semicolon at the end of its statement is reached:

std::string("Temporary"); // Destroyed immediately at the end of this full-expression

When bound to a local reference, the destruction of the temporary object is deferred:

{
    const std::string& ref = std::string("Temporary");
    // The temporary object remains valid throughout this entire block scope.
} // The temporary object is destroyed here, when 'ref' goes out of scope.

The same behavior applies to rvalue references:

std::string&& rref = std::string("Temporary"); // Lifetime extended to match 'rref'

Non-const lvalue references (T&) cannot bind to temporaries at all, except through non-standard compiler extensions.

Sub-objects and Arrays

Lifetime extension is transitive across member access and array indexing. If a reference binds to a sub-object of a temporary (such as a public member variable) or an element of a temporary array, the entire parent temporary object has its lifetime extended:

struct Pair {
    int first;
    int second;
};

const int& ref = Pair{1, 2}.first; // The entire 'Pair' temporary survives as long as 'ref'

Exceptions Where Lifetime Is Not Extended

Lifetime extension is not universal. The C++ standard explicitly defines several scenarios where binding a temporary to a reference does not extend its lifetime, often leading to undefined behavior caused by dangling references.

1. Function Return Statements

A temporary bound to a reference returned by a function does not outlive the return statement:

const std::string& bad_function() {
    return std::string("Dangling"); // UB: temporary destroyed at the end of the return statement
}

The temporary is destroyed when the function returns, leaving the caller with a dangling reference.

2. Function Parameters

When a temporary is passed to a function that takes a reference parameter, the temporary's lifetime is only extended to the end of the full-expression containing the function call, not to the lifetime of the parameter itself:

void print(const std::string& str);

print(std::string("Hello")); // Temporary is destroyed immediately after 'print' finishes executing

3. Class Member Initialization

If a temporary is bound to a reference member in a constructor initializer list, the temporary's lifetime is extended only until the constructor finishes executing, not for the lifetime of the object:

struct Holder {
    const std::string& ref;
    Holder() : ref(std::string("Temporary")) {} // UB: temporary destroyed when constructor exits
};

Once the constructor completes, ref becomes a dangling reference inside the newly created Holder instance.

4. Indirect Binding via Functions

Lifetime extension only occurs when a temporary is directly bound to a reference. If a temporary is passed through a function that returns a reference, the returned reference cannot extend the original temporary's lifetime:

const std::string& identity(const std::string& s) { return s; }

const std::string& ref = identity(std::string("Temporary")); 
// UB: The temporary is destroyed at the end of the line; 'ref' is dangling.

5. New Expressions

If a temporary is bound to a reference inside a new initializer, the temporary is destroyed at the end of the full-expression containing the allocation, rather than surviving for the lifetime of the heap-allocated object:

struct Node {
    const int& val;
};

Node* n = new Node{10}; // UB: The temporary int(10) is destroyed after this line