Temporaries Lifetime
How C++ temporaries are created, when they are destroyed, how lifetime extension works, and the dangling-reference traps that follow from the rules.
Temporary objectsince C++98A temporary is an unnamed object created during expression evaluation, and it is destroyed at the end of the enclosing full-expression unless it is bound to a reference that extends its lifetime.
Overview
Temporaries arise from prvalue expressions that must be materialized as objects: function returns by value, conversions, T(args) expressions, arithmetic on class types, and braced-init lists bound to references. Their lifetime is tied to the full-expression in which they are created, not to the statement's apparent scope or the enclosing block.
Key rules:
- Destruction point. Temporaries are destroyed at the end of the full-expression, in reverse order of creation. A full-expression is typically everything up to the
;. - Value category. Since C++11 an rvalue reference or
const T&can bind to a temporary. A non-const lvalue reference cannot. - Lifetime extension. Binding a temporary directly to a reference variable extends its lifetime to that of the reference. This applies to automatic, static and thread-local references and to reference members only in limited cases (see below).
- Guaranteed copy elision (C++17). A prvalue no longer creates a temporary that is then moved. It initializes its target directly. Temporary materialization occurs only when a prvalue is used where a glvalue is needed, for example when binding to a reference or accessing a member.
Extension rules in detail
Extension applies when a reference is initialized from:
- a prvalue (
const std::string& r = make();); - a member access, array subscript or derived-to-base conversion applied to such a temporary (
const auto& n = make().name;extends the whole object); - an aggregate initializer's reference member bound directly to a temporary (
struct S { const int& r; }; S s{5};; the temporary lives as long ass).
Extension does not apply when:
- the temporary is bound to a function parameter. It lives until the end of the full-expression containing the call, not beyond the call;
- the temporary is bound to a reference member in a constructor's mem-initializer list. It dies when the constructor exits (and since C++20 such a binding is ill-formed only when the compiler can detect it, so do not rely on diagnostics);
- the reference is returned from a function. The result dangles;
- the temporary is the result of a function call that returns a reference. The referred object's lifetime is not affected.
C++23 (P2718R0) changes range-based for: temporaries in the range-initializer now live for the entire loop. Before C++23, for (auto& x : f().items()) is undefined behavior if items() returns a reference into the temporary returned by f().
Syntax
#include <string>
std::string make();
void demo() {
make(); // temporary destroyed at ';'
const std::string& a = make(); // C++98: extended to a's lifetime
std::string&& b = make(); // C++11: extended, modifiable
// std::string& c = make(); // error: non-const lvalue ref to rvalue
std::string s = std::string("x") + "y" + "z";
// two intermediate temporaries, both destroyed at the ';'
}Examples
Ordering and destruction timing
#include <cstdio>
struct Tracer {
const char* name;
explicit Tracer(const char* n) : name(n) { std::printf("ctor %s\n", name); }
~Tracer() { std::printf("dtor %s\n", name); }
int id() const { return 1; }
};
int combine(int a, int b) { return a + b; }
int main() {
int r = combine(Tracer("A").id(), Tracer("B").id());
// Both temporaries live until the ';'. The construction order of A and B
// is unspecified, but destruction is the reverse of whatever it was.
std::printf("r=%d\n", r);
}Lifetime extension through a member
#include <string>
struct Config {
std::string name;
std::string path;
};
Config load();
void use() {
const std::string& n = load().name;
// The entire Config temporary is extended because we bound a member of it.
// Safe, but surprising: path stays alive too.
}The dangling pass-through
#include <string>
#include <algorithm>
const std::string& pick(const std::string& a, const std::string& b) {
return a.size() >= b.size() ? a : b;
}
void bad() {
const std::string& r = pick("alpha", "beta");
// "alpha" and "beta" are temporary std::strings bound to parameters.
// They are destroyed at the end of this full-expression.
// r dangles: using it is undefined behavior. Extension does not
// propagate through function calls.
}
std::string good(const std::string& a, const std::string& b) {
return a.size() >= b.size() ? a : b; // return by value: copy is safe
}Views and string_view (C++17)
#include <string>
#include <string_view> // C++17
std::string name();
void f() {
std::string_view v = name();
// v dangles immediately: the temporary std::string dies at the ';'.
// string_view never extends lifetime; it is not a reference.
std::string owner = name();
std::string_view ok = owner; // fine while owner lives
}The same hazard applies to std::span (C++20), ranges views (C++20), and std::format_string captures stored beyond the call.
Range-for pitfall and fix
#include <vector>
struct Registry {
std::vector<int> ids_;
const std::vector<int>& ids() const { return ids_; }
};
Registry make_registry();
void iterate() {
// UB before C++23: Registry temporary dies before the loop body runs.
for (int id : make_registry().ids()) { /* ... */ }
// Portable fix (all versions):
Registry reg = make_registry();
for (int id : reg.ids()) { /* ... */ }
}Since C++23 the first form is well-defined because every temporary in the range-initializer is extended through the loop.
Safe use of a temporary as a function argument
#include <string>
#include <cstdio>
void log(const std::string& msg) { std::printf("%s\n", msg.c_str()); }
std::string describe(int code);
void run() {
log(describe(42) + " (retry)"); // OK: temporaries outlive the call
}Forwarding references and auto&&
#include <vector>
std::vector<int> produce();
void consume() {
auto&& v = produce(); // C++11: extends lifetime; deduced as vector<int>&&
v.push_back(1); // v is an lvalue expression and modifiable
}auto&& is the universal way to bind "whatever comes back" without copying while keeping lifetime guarantees for prvalues. For returned lvalue references it simply aliases the original.
Best Practices
- Prefer returning by value. Since C++17 the result is constructed directly into its destination, and before that NRVO and moves usually apply.
- Take
const T&orT&¶meters freely. The argument temporary lives for the entire call. - Never store a reference or view derived from a function argument unless the API contract requires the caller to keep the object alive. Document that contract.
- Bind long-lived results to named variables before taking views, iterators or pointers into them.
- In generic code, use
auto&&for range-for and for capturing function results. - Avoid chaining member functions that return references into a temporary (
make().items().front()) when the result outlives the statement. Copy the element out within the same full-expression. - Use
= deleteon rvalue-qualified overloads to block misuse:
#include <string>
class Holder {
std::string data_;
public:
const std::string& get() const& { return data_; } // C++11 ref-qualifier
const std::string& get() && = delete; // forbid on temporaries
std::string take() && { return std::move(data_); }
};- Enable
-Wdangling,-Wdangling-reference(GCC 13+) and AddressSanitizer withdetect_stack_use_after_scope; these catch many of the cases above.
Common Pitfalls
- Believing extension is transitive. It applies only when the reference is initialized directly from the temporary (or a subobject). Passing through any function, even an identity function, breaks it.
- Reference members.
struct W { const std::string& s; W(const char* p) : s(p) {} };bindssto a temporary that dies when the constructor returns. Store by value or require an lvalue. autostripping references.auto x = f();copies.const auto& x = f();extends. Swapping them silently changes both cost and aliasing behavior.- Ternary operator.
const std::string& r = cond ? a : std::string("x");yields a prvalue, soais copied into a temporary whencondis true.ris not an alias fora. - Conditional temporaries and short-circuit. Temporaries created in the right operand of
&&,||or?:exist only if that operand is evaluated, and are still destroyed at the end of the full-expression. - Implicit conversions. Passing a
const char*to aconst std::string¶meter creates a temporary on every call, which has a cost in hot paths. Add an overload or takestd::string_view(C++17). - Temporaries in
std::thread/std::asyncarguments. Arguments are decay-copied into the new thread's storage, so the reference-to-temporary issue differs: usestd::ref(C++11) only for objects that outlive the thread. - Cached
end()and iterator temporaries. Repeatedcontainer.end()in a loop condition is usually optimized away, but for non-trivial iterators hoist it, or prefer range-for. - Unevaluated or
decltypecontexts. No temporary is created in unevaluated operands, butdecltype(f())for a prvalue of class type does not require a complete destructor-accessible type until C++11 rules (it never materializes the object). - Exception safety. Any hidden temporary construction may throw. When two temporaries are created in one expression, an exception during the second still destroys the first, but argument evaluation order is unspecified before C++17 and indeterminately sequenced after, so avoid
f(std::shared_ptr<T>(new T), g()); usestd::make_shared(C++11).