Skip to content
C++
Language
Basic

Storage Duration

The four lifetime categories of object storage in C++ — automatic, static, dynamic, and thread — and what determines each.

Storage Durationsince C++98

Storage 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.

DurationProduced byLifetime
AutomaticLocal variable without staticEnclosing block
StaticNamespace scope; static; externProgram lifetime
Dynamicnew expression; placement-newExplicit control
Threadthread_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

cpp
// 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.

cpp
#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 thrown

Construct-on-First-Use for Static Objects

cpp
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

cpp
#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

cpp
#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)

cpp
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

cpp
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.

cpp
// 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 yet

Fix: replace global variables with function-local statics.

cpp
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 first

Thread-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.

cpp
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 captured

This 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 variables
  • reference/language/const-correctnessconst objects with static duration and their initialisation guarantees
  • reference/language/constant-expressionsconstexpr and constinit (C++20) for static-duration initialisation