C++ Object Lifetime and Storage Duration: Trace Construction, Destruction, and Dangling State

Learn to separate scope, storage duration, lifetime, and ownership. Trace seven objects precisely, then repair dangling pointers, references, and views.

KnowledgeGate Team

Exam prep & CS education

Updated 19 Oct 20265 min read

A pointer can retain an address after its object dies, a reference can outlive what it names, and a static object can be alive at the wrong point in startup or shutdown. Such bugs resemble corrupted state even when the assignment was correct. C++ has four storage durations, constructor and destructor traces can be predicted, and dangling designs can be repaired with RAII. Ask not only "where are the bytes?" but "when does this object begin and end, and which handles may still observe it?"

C++ scope, storage duration, and lifetime

Scope controls name use, storage duration the storage period, and lifetime object existence. Block-local int x = 41; has block scope, automatic storage, and lifetime from completed initialisation to block exit.

Lifetime begins after storage is obtained and initialisation completes. It ends when destruction starts or storage is released or reused. Remaining bytes prove nothing.

A class defines a type; its objects have lifetimes, while pointers, references, iterators, spans, and views may only observe them. For name lookup, namespaces, and linkage across files, use C++ Variables, Scope and Namespaces: Trace Name Lookup Across Three Files. The lifetime analysis here begins after lookup resolves a name to an object: decide when that object exists and when each observer becomes invalid.

Four C++ storage durations

These are language categories. "Stack" and "heap" are not synonyms for them.

Storage duration

Typical declaration

Storage lasts

Object ends in this example

Ownership lesson

automatic

Trace automatic{3};

entry to exit of its enclosing block, subject to normal language rules

at the closing brace of main

local names do not make borrowed pointers safe after return

static

static Trace once{6};

for the program

during normal program termination, after the object has been initialised

long storage does not guarantee safe cross-file initialisation or teardown dependencies

thread

thread_local Trace per_thread{2};

separately for each thread that has an instance

when that thread exits

it is per-thread state, not one process-wide singleton

dynamic

auto heap = std::make_unique<Trace>(5);

until the allocated storage is released

when heap destroys its managed object at the inner block exit

bind dynamic lifetime to an RAII owner rather than a bare owning pointer

Namespace-scope file_scope{1} also has static duration, but block-static once{6} is first initialised when reached. The owner heap is automatic; its managed Trace{5} is dynamic.

Exact C++ construction and destruction trace

cpp
#include <iostream>
#include <memory>
#include <thread>

struct Trace {
    int id;
    explicit Trace(int value) : id{value} { std::cout << '+' << id << ' '; }
    ~Trace() { std::cout << '-' << id << ' '; }
};

Trace file_scope{1};

Trace& thread_value() {
    thread_local Trace per_thread{2};
    return per_thread;
}

void use(const Trace&) { std::cout << "use "; }

int main() {
    std::cout << "main ";
    Trace automatic{3};
    {
        Trace inner{4};
        auto heap = std::make_unique<Trace>(5);
        std::cout << "block ";
    }
    static Trace once{6};
    std::thread worker([] {
        Trace worker_auto{7};
        use(thread_value());
    });
    worker.join();
    std::cout << "return ";
}

The exact output is:

Code
+1 main +3 +4 +5 block -5 -4 +6 +7 +2 use -7 -2 return -3 -6 -1

ID 1 appears before main. IDs 4 then 5 enter the block and leave as 5 then 4. ID 6 comes next. The worker creates ID 7 before its ID 2. ID 7 dies on function return; ID 2 at thread exit.

After join, main return destroys ID 3. Normal termination destroys ID 6 before ID 1. This excludes std::quick_exit, std::terminate, abnormal death, and undefined behaviour.

It is deterministic because main outputs nothing from worker start to join. Unsynchronised writers have no such guarantee. Main never calls thread_value(), so creates no ID 2.

Lifetime timeline of the seven Trace objects across program events, with the printed +1 to -1 output sequence below the lanes.

C++ temporary and view lifetime limits

const int& kept = int{64}; extends its directly bound temporary until kept leaves scope. Returning const std::string& to std::string{"KG64"} does not: the temporary dies when the return expression ends. Never read it.

std::string_view bad_view() { std::string local{"KG42"}; return local; } returns a live view with a dead four-character owner. Borrows extend no owner lifetime. Use std::string owner{"KG42"}; std::string_view view{owner}; consume(view); before destruction or invalidating modification. Copying the view copies only the borrow.

Audit dangling C++ handles by owner

Handle still present

Exact owner

Owner-ending event

Why the handle dangles

Repair

const int* p

local int value{41}

function returns

the automatic object has ended even if p still contains its former address

return 41 by value

std::string_view view

local std::string{"KG42"}

function returns

the view owns no characters

return std::string{"KG42"} by value, or accept a view only for a call whose owner remains alive

int* raw

dynamic new int{73}

delete raw

raw remains non-null but no live int exists there

use auto owner = std::make_unique<int>(73) and pass a non-owning pointer only inside owner's lifetime

Never dereference, compare, or print through stale handles. raw = nullptr protects no copied alias. RAII removes manual deletion, not observer checks. Pointers in C for GATE explains address versus value; this repair uses C++ RAII.

Three panels: a pointer to a returned local, a string_view outliving its string, and a raw pointer after delete beside a unique_ptr owner.

C++ initialisation and teardown traps

Member declaration order controls construction, not the order written in a constructor's list:

cpp
struct Pair {
    Trace first;
    Trace second;
    Pair() : second{9}, first{8} {}
};

Declaration order, not the mem-initialiser list, constructs first, second as +8 +9; destruction is -9 -8.

Across translation units, source layout gives no universal startup order for dynamic statics A and B. Registry& registry() { static Registry r{42}; return r; } constructs r on first call, thread-safely since C++11.

Teardown still fails if a static destructor calls a dead static. A thread_local handle dangles when its thread exits while others continue.

Four-step C++ lifetime solve routine

Questions ask for storage classes, exact output, an owner-handle mismatch, leak versus dangling state, or RAII. The C++ Tutorial: The Complete Learning Path from First Program to STL covers the surrounding references, OOP, STL, and memory topics in order.

For every snippet:

  1. Mark each owner.

  2. Label its storage automatic, static, thread, or dynamic.

  3. Write the exact event that begins and ends each object.

  4. Inspect every pointer, reference, view, iterator, callback capture, and destructor dependency that crosses an end event.

Then predict output or undefined behaviour. Checks: Trace a{10}; Trace b{11}; destroys as -11 -10; deleting int{73} may leave raw non-null and dangling; returning std::string{"KG42"} by value gives ownership.

C++ object lifetime: short version

  • Scope tells where a name is visible.

  • Storage duration tells how long storage is reserved.

  • Lifetime tells when an object exists there.

  • Ownership tells who ends it and whether observers may still use it.

If an observer crosses its owner's end event, redesign the interface.

Reproduce IDs 1 to 7. Then move once{6} before the inner block, reverse 4 and 5 with RAII, and call thread_value() in main. Predict first; main and worker ID 2 are separate.

For structured concepts, MCQs, and coding practice, see the C++ Programming course.