Storage Duration
The four lifetime categories of object storage in C++ — automatic, static, dynamic, and thread — and what determines each.
Storage Durationsince C++98Storage duration is the property of an object that defines the minimum potential lifetime of the storage containing it, determined entirely by the construct used to create the object.
Overview
Every object in C++ belongs to exactly one of four storage duration categories. The category controls when memory is acquired, when it is released, and how the runtime initialises the object. Storage duration is independent of scope: a function-local static variable has block scope but program-length storage.
| Duration | Produced by | Lifetime |
|---|---|---|
| Automatic | Local variable without static | Enclosing block |
| Static | Namespace scope; static; extern | Program lifetime |
| Dynamic | new expression; placement-new | Explicit control |
| Thread | thread_local (C++11) | Thread lifetime |
Automatic Storage Duration
Local variables declared without static or extern have automatic storage duration. Storage is allocated when execution enters the enclosing block and released on exit. The implementation is typically a stack-pointer adjustment — effectively free compared to heap allocation.
Destructors of automatic objects run in reverse construction order, and this is guaranteed to happen even during stack unwinding caused by an exception. Automatic duration is the physical foundation of RAII.
Static Storage Duration
Objects with static storage duration persist for the entire program. Three constructs produce them:
- Variables at namespace scope (including file-scope globals)
- Local variables declared
static - Variables declared
extern
Static-duration objects are zero-initialised before any other initialisation. For constant initialisers this is followed by constant initialisation at compile time. For non-constant initialisers, dynamic initialisation occurs either before main (for namespace-scope variables) or on first execution of the declaration (for function-local statics). Since C++11, the initialisation of function-local statics is guaranteed thread-safe — the runtime serialises concurrent first-callers.
Dynamic Storage Duration
Objects created with new expressions have dynamic storage duration. Their lifetime begins at allocation and ends at the matching delete. The programmer controls the lifetime entirely, which is exactly where memory bugs originate: leaks, double-frees, and use-after-free all stem from mismanaged dynamic duration.
Since C++11, raw new/delete in application code is a code smell. Wrap dynamic objects in std::unique_ptr (C++11) or std::shared_ptr (C++11) and let ownership semantics handle the delete.
Thread Storage Duration (C++11)
The thread_local specifier creates one object instance per thread. Each instance is allocated when its thread starts and destroyed when the thread ends. Initialisation follows the same rules as static duration, scoped to the thread. thread_local can be combined with static or extern to control linkage without changing the per-thread lifetime.
Syntax
// Automatic — local variable, no specifier
void f() {
int x = 0; // automatic storage duration
std::string s; // automatic; destructor called when block exits
}
// Static — namespace scope
int request_count = 0; // static storage duration, external linkage
// Static — file scope
static int module_id = 42; // static storage duration, internal linkage
// Static — function-local (thread-safe init since C++11)
int& next_id() {
static int id = 0; // initialised once across all calls
return ++id;
}
// Static — class member (definition outside the class)
struct Config {
static int max_connections; // declaration
};
int Config::max_connections = 64; // definition, static storage duration
// Dynamic
void g() {
int* raw = new int(42); // dynamic storage duration
delete raw; // explicit release required
auto p = std::make_unique<int>(42); // C++14 — preferred
} // p's destructor calls delete; no leak
// Thread-local (C++11)
thread_local int errno_cache = 0;
// Thread-local with internal linkage (C++11)
static thread_local std::vector<char> scratch;Examples
RAII over Automatic Duration
Tying resource ownership to an automatic object's lifetime makes cleanup unconditional.
#include <fstream>
#include <mutex>
std::mutex log_mutex;
void append_log(std::string_view msg) { // std::string_view: C++17
std::lock_guard<std::mutex> lock{log_mutex}; // C++11 — automatic
std::ofstream out{"app.log", std::ios::app}; // automatic
out << msg << '\n';
} // out closed, then lock released — guaranteed, even if an exception was thrownConstruct-on-First-Use for Static Objects
class ConnectionPool {
public:
static ConnectionPool& instance() {
static ConnectionPool pool; // C++11: guaranteed thread-safe initialisation
return pool;
}
void acquire() { /* ... */ }
private:
ConnectionPool() { /* expensive one-time setup */ }
};
void worker() {
ConnectionPool::instance().acquire();
}The function-local static is zero-initialised before main, then its constructor runs on the first call. Subsequent calls skip initialisation entirely. This pattern sidesteps the static initialisation order fiasco (see Pitfalls).
Thread-Local Request Context
#include <cstdint>
struct RequestCtx {
uint64_t request_id{};
int user_id{-1};
};
thread_local RequestCtx current_ctx; // C++11 — one instance per thread
void set_request(uint64_t rid, int uid) {
current_ctx = {rid, uid};
}
uint64_t get_request_id() {
return current_ctx.request_id; // reads this thread's copy
}Each thread pool thread gets its own current_ctx. No locking required because no sharing occurs.
Unique Ownership of Dynamic Objects
#include <memory>
struct Node {
int value;
std::unique_ptr<Node> left, right; // C++11
};
std::unique_ptr<Node> build_tree(int depth) {
if (depth == 0) return nullptr;
auto node = std::make_unique<Node>(depth); // C++14
node->left = build_tree(depth - 1);
node->right = build_tree(depth - 1);
return node; // ownership transferred; caller's unique_ptr takes over
}
// When the root goes out of scope, the entire tree is destroyed recursively.constinit for Static Variables (C++20)
constinit int thread_pool_size = 8; // C++20 — enforces constant initialisation
// constinit int size = compute_size(); // error: not a constant expression
// Without constinit, this would silently become dynamic init with ordering hazards.constinit doesn't make the variable const — it mandates that initialisation happens at compile time, eliminating dynamic-init ordering issues.
Best Practices
Default to automatic duration. Stack allocation costs nothing and cannot leak. Move to dynamic duration only when object size is unknown at compile time, lifetime must outlast the current scope, or the object is too large for the stack.
Use std::unique_ptr as the default owner of dynamic objects. std::shared_ptr is appropriate only when ownership is genuinely shared across independent parties; shared ownership adds atomic reference-counting overhead and makes reasoning about lifetime harder, not easier.
Use function-local statics to control initialisation order. If a static-duration object's initialiser depends on another, wrap both in functions returning references to local statics. This defers initialisation to first use, in deterministic order.
Mark thread-local variables with non-trivial constructors carefully. In a long-lived thread pool, thread-local construction happens once per thread — fine. In a workload that spawns thousands of short-lived threads, constructor overhead per thread can accumulate.
Audit lambda captures involving automatic variables. A lambda that escapes its creation scope (stored in a std::function, posted to an executor, or returned) must not capture automatic variables by reference. The reference dangles when the enclosing block exits.
Common Pitfalls
Dangling Reference to Automatic Storage
int* get_value() {
int x = 42;
return &x; // undefined behaviour: x is destroyed on return
}
std::string_view get_view() { // C++17
std::string s = "hello";
return s; // undefined behaviour: s is destroyed, view dangles
}Compilers warn about the pointer case. The string_view case is subtler and often goes undiagnosed.
Static Initialisation Order Fiasco
Namespace-scope variables in different translation units with dynamic initialisers have unspecified relative initialisation order. Reading one from another's initialiser is undefined behaviour.
// file_a.cpp
int limit = compute_limit(); // dynamic init
// file_b.cpp
extern int limit;
int adjusted = limit + 10; // undefined: limit may not be initialised yetFix: replace global variables with function-local statics.
int get_limit() {
static int v = compute_limit(); // initialised on first call (C++11: thread-safe)
return v;
}
int get_adjusted() { return get_limit() + 10; } // safe: get_limit() called firstThread-Local Variables Are Not Captured by Lambdas
[=] and [&] capture only automatic-duration variables. Static and thread-local variables are accessed directly inside the lambda body — they are not copies.
thread_local int tval = 10;
auto f = [=]() {
return tval; // NOT a captured copy — reads current thread's tval at call time
};
tval = 99;
f(); // returns 99, not 10 — tval was not capturedThis is frequently surprising when a lambda is posted to a different thread: tval in the lambda refers to the executing thread's copy, not the creating thread's copy.
See Also
reference/language/auto— type deduction interacts with automatic-duration variablesreference/language/const-correctness—constobjects with static duration and their initialisation guaranteesreference/language/constant-expressions—constexprandconstinit(C++20) for static-duration initialisation