Object Lifetime
How C++ defines when objects begin and end their lives: storage duration, initialization, destruction order, temporaries, lifetime extension, and dangling references.
Object lifetimesince C++98The lifetime of an object is the runtime interval that begins when its storage is obtained and its initialization is complete, and ends when its destructor call starts (or, for trivially destructible types, when its storage is released or reused); accessing an object outside this interval is undefined behavior.
Overview
Lifetime is a runtime property, distinct from scope (a compile-time region where a name is visible) and from storage duration (how long the underlying bytes are reserved). An object can be in scope but not yet alive, or alive but out of scope (a heap object whose owning pointer has left its block).
C++ has four storage durations:
| Storage duration | Storage obtained | Object destroyed | Typical examples |
|---|---|---|---|
| Automatic | Entry to enclosing block | Exit from block, reverse order of construction | Locals, function parameters |
| Static | Program start (or first use for block-scope statics) | Program exit, reverse order of construction completion | Globals, static locals, class statics |
| Thread (C++11) | Thread start | Thread exit | thread_local variables |
| Dynamic | new / allocation function | delete / explicit destructor call | new T, std::make_unique<T>() |
Key rules:
- Members are constructed in declaration order and destroyed in the reverse order. Base classes are constructed before members, and destroyed after them.
- Static initialization order across translation units is unspecified. Block-scope statics, by contrast, are initialized on first control passage, and since C++11 this is thread-safe.
- Constant initialization (
constexpr, andconstinitsince C++20) happens at compile time, so it avoids the static initialization order fiasco entirely. - Reuse of storage ends the lifetime of the previous object. Pointers and references to the old object no longer refer to the new one unless it is transparently replaceable. Use
std::launder(C++17) when needed. - Implicit-lifetime types (C++20, applied retroactively to earlier standards as a defect resolution) such as arrays and trivial aggregates may be implicitly created by
malloc,std::memcpy, andoperator new.
Syntax
#include <memory>
#include <new>
struct Widget { int id; };
void forms() {
Widget a{1}; // automatic
static Widget b{2}; // static, initialized on first call (thread-safe since C++11)
thread_local Widget c{3}; // thread storage duration, since C++11
Widget* d = new Widget{4}; // dynamic
delete d;
alignas(Widget) unsigned char buf[sizeof(Widget)];
Widget* e = new (buf) Widget{5}; // placement new: lifetime begins
e->~Widget(); // explicit destruction: lifetime ends
}Temporaries and lifetime extension
A temporary normally dies at the end of the full-expression that created it. Binding it to a const T& or T&& local extends its lifetime to that of the reference:
#include <string>
std::string make();
void f() {
const std::string& r = make(); // temporary lives until r goes out of scope
std::string&& rr = make(); // same
} // both destroyed hereExtension does not propagate through function calls, and does not apply to return values or to members accessed through a returned reference.
Examples
Destruction order and RAII
#include <cstdio>
#include <string>
class Trace {
std::string name_;
public:
explicit Trace(std::string n) : name_(std::move(n)) { std::printf("ctor %s\n", name_.c_str()); }
~Trace() { std::printf("dtor %s\n", name_.c_str()); }
Trace(const Trace&) = delete;
};
struct Pair {
Trace first{"first"};
Trace second{"second"};
};
int main() {
Trace a{"a"};
Pair p;
Trace b{"b"};
} // prints: dtor b, dtor second, dtor first, dtor aBecause destruction is deterministic and in reverse order, RAII types can depend on resources acquired earlier in the same scope. Keep lifetimes narrow: declare locks, files, and connections as late as possible and let scope release them.
Ownership transfer
#include <memory>
#include <vector>
struct Connection { /* ... */ };
std::unique_ptr<Connection> open(); // factory: caller owns, C++11
class Pool {
std::vector<std::unique_ptr<Connection>> conns_;
public:
void add(std::unique_ptr<Connection> c) { conns_.push_back(std::move(c)); }
Connection& at(std::size_t i) { return *conns_[i]; } // reference valid while Pool lives
};std::make_unique is available since C++14; in C++11 write std::unique_ptr<T>(new T(...)).
Observing without owning
#include <memory>
struct Node;
struct Node {
std::shared_ptr<Node> next; // owning
std::weak_ptr<Node> parent; // non-owning: breaks the shared_ptr cycle
};
void use(const std::weak_ptr<Node>& w) {
if (auto sp = w.lock()) { // atomically check and acquire
// sp is safe to use here
}
}Never write if (!w.expired()) w.lock()->...; the object can die between the two calls in a multithreaded program.
Fixing a static initialization order problem
// C++11: function-local static gives guaranteed order and thread safety
Registry& registry() {
static Registry r;
return r;
}
// C++20: compile-time initialization, no dynamic init at all
constinit std::atomic<int> counter{0};Best Practices
- Prefer automatic storage. If an object doesn't need to outlive its block, don't allocate it dynamically.
- Default to
std::unique_ptrfor dynamic objects (C++11). Reach forstd::shared_ptronly when ownership is genuinely shared, and break cycles withstd::weak_ptr. - Never own through raw
new/delete. Encapsulate in a RAII type so exceptions cannot leak. - Make non-owning pointers and references explicit in interfaces (
T&,T*,std::spansince C++20,std::string_viewsince C++17), and document what they borrow from. - Apply the Rule of Zero, or the Rule of Five when a class owns a resource, so that copies don't produce two owners of one allocation.
- Use
constexpr/constinit(C++11 / C++20) for globals to avoid cross-TU initialization-order hazards. - Run sanitizers. AddressSanitizer catches use-after-free and use-after-scope;
-fsanitize=addressplusdetect_stack_use_after_return=1handles the return-of-local case.
Common Pitfalls
Returning references or pointers to locals
const std::string& bad() {
std::string s = "hello";
return s; // UB on use: s is destroyed on return
}Compilers usually warn (-Wreturn-local-addr), but not when the address passes through another function.
Dangling views of temporaries
#include <string>
#include <string_view>
std::string name();
std::string_view v = name(); // C++17: dangles immediately, temporary destroyed at ';'
const std::string& ok = name(); // fine: lifetime extendedThe same applies to std::span (C++20) over a temporary container, and to iterating for (auto x : make().items()) in a range-for: before C++23, only the outermost temporary is extended, so inner temporaries die before the loop starts. Since C++23 (P2718), all temporaries in the range-initializer are extended.
Lambdas capturing by reference
#include <functional>
std::function<int()> counter() {
int n = 0;
return [&n] { return ++n; }; // C++11: n dies on return; use [n]() mutable instead
}Only automatic variables can be captured. A lambda stored in a thread, a callback registry, or a returned std::function must not hold references to locals.
Invalidated iterators and references
std::vector::push_back that reallocates ends the lifetime of every old element (they are moved and destroyed), so all existing pointers, references, and iterators into it are invalid. The same applies to std::string mutation and to std::unordered_map rehash (iterators invalidated, but references to elements stay valid).
Use after move
A moved-from object is alive and valid, but in an unspecified state (for standard types). You may destroy or assign to it; don't assume its value. Moving does not end the lifetime.
Mismatched deallocation and double free
Memory from new must be released with delete, and from new[] with delete[]; malloc memory with free. Mixing them is undefined behavior. Copying a raw-owning class without a proper copy constructor leads to two owners and a double delete.
Exceptions in constructors and destructors
If a constructor throws, the object's lifetime never began: its destructor does not run, but fully constructed subobjects are destroyed. Destructors are implicitly noexcept since C++11; throwing from one during unwinding calls std::terminate.
Static destruction order
Static objects are destroyed in reverse order of construction completion, but a background thread or another static's destructor may still use an already-destroyed object. Avoid logging or touching singletons from static destructors, or use an intentionally leaked heap singleton.