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-expressionWhen 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 executing3. 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