Facebook Pixel

Object Lifetime: When C++ Objects Begin and End

Dangling pointer bugs in C++ are notorious because they rarely crash right away. When a function returns, the CPU doesn't zero out memory; it just bumps the stack pointer register (add $0x20, %rsp). Whatever struct was sitting in that stack frame stays untouched until the next function call pushes a new frame and clobbers those bytes.

In a shallow test or single-threaded benchmark, reading through a dangling pointer often returns the exact data you just wrote. The bug only explodes in production when an unrelated thread or nested call stack writes over that memory.

Here is the classic pattern where this bites in message processing:

const char* active_symbol = nullptr;

void handle_message(const MessageBuffer& buf) {
    std::string symbol = parse_symbol(buf); // Local std::string on the stack
    active_symbol = symbol.c_str();         // Points to internal buffer (SSO or heap)
} // symbol destructor runs here; heap buffer freed or SSO stack memory invalidated

active_symbol holds the address where the characters lived. Once handle_message returns, that memory is invalid. If symbol used small-string optimization (SSO), the characters sat on the now-discarded stack frame. If it allocated on the heap, ~std::string() freed the buffer back to the allocator. Either way, reading active_symbol is undefined behavior.

Stack frames and LIFO destruction

Stack-allocated (automatic) objects live and die strictly with their enclosing scope ({ ... }). Construction runs at the declaration line; destruction runs at the closing brace.

When a scope contains multiple local variables, the compiler destroys them in the exact reverse order of their construction:

void process_feed() {
    SpinLock mutex;
    LockGuard guard(mutex);   // 1. guard references mutex
    OrderBook book;           // 2. book initialized
    
    // Process orders...
} // Teardown: 1. book destructs, 2. guard releases mutex, 3. mutex destructs

This LIFO destruction rule exists for dependency safety. If guard holds a reference to mutex, tearing down mutex first would leave guard dereferencing dead state during its own destructor. Reverse destruction guarantees that any resource initialized earlier in a block remains valid while dependent objects shut down.

The temporary lifetime trap with string_view

Temporaries generated during expression evaluation live only until the end of the full expression—the semicolon.

In modern C++, the most common lifetime bug involves non-owning views like std::string_view or std::span capturing temporary objects:

// BUG: std::string temporary is destroyed at the semicolon!
std::string_view sv = build_full_ticker(exchange, symbol);
send_to_exchange(sv); // sv points to deallocated memory

build_full_ticker returns a std::string by value. sv binds to its internal data buffer. At the semicolon, ~std::string() runs immediately. By the time execution reaches send_to_exchange, sv is already pointing at dead memory.

C++ has one exception to this rule: binding a temporary to a local const auto& or auto&& on the stack extends the temporary's lifetime to match the reference's scope:

// Lifetime extended: the temporary std::string lives as long as 'msg'
const auto& msg = build_full_ticker(exchange, symbol);
send_to_exchange(msg); // Safe: msg is still alive here

However, this extension never propagates across a function return boundary. Returning a const std::string& to a local temporary still dangles the moment the function returns.

Addresses do not imply ownership

A raw pointer or reference is just a 64-bit integer containing a virtual memory address. The hardware has no reference counter or runtime GC mechanism tracking who is pointing at what.

Diagram showing local variables constructed in a scope and destroyed in reverse order, while a stale pointer is left referencing invalidated stack storage.
Stack objects are destroyed in reverse declaration order at block exit; external pointers retain stale addresses.

When an object dies, any pointer holding its address immediately dangles. To catch these in CI and development, compile with AddressSanitizer flags:

clang++ -fsanitize=address -fsanitize-address-use-after-scope -g main.cpp

ASan poisons the stack bytes when execution exits a scope, triggering an immediate crash with a diagnostic stack trace if code touches the stale memory.

See it run

Run this example to watch construction and destruction order, and notice how the heap object requires manual destruction:

Returning values: why return-by-value is zero-copy

Older C++ code often returned objects via pointers or out-parameters to avoid the perceived overhead of copying. In modern C++, that practice is obsolete and dangerous.

// Dangerous: returns a reference to stack memory that dies on return
const MarketUpdate& get_latest_quote() {
    MarketUpdate quote = parse_quote();
    return quote; // Compiler warning: reference to stack memory returned
}

// Correct: Return by value
MarketUpdate get_latest_quote() {
    MarketUpdate quote = parse_quote();
    return quote; // Zero-copy construction via NRVO / RVO
}

Since C++17, prvalue return-by-value benefits from guaranteed copy elision (RVO). The compiler does not construct the object inside the callee's stack frame and copy it across the return; it passes a pointer to the caller's pre-allocated stack space (usually in %rdi on SysV x86-64) and constructs the object directly in place. Returning by value guarantees safety with zero runtime copying cost.

Checkpoint

1)

A function constructs a local std::string and returns a std::string_view referencing it. What happens when the caller accesses that string_view?

In interviews

  • "What happens under the hood when a pointer outlives a stack variable?" → "The pointer retains the virtual address, but the stack pointer register (%rsp) moves up when the function returns. The memory is not wiped—it simply waits for the next stack frame to overwrite it. Dereferencing it is undefined behavior, caught by -fsanitize=address -fsanitize-address-use-after-scope."
  • "Why does C++ guarantee LIFO destruction for local variables?" → "Because objects declared later frequently depend on resources initialized earlier—like a lock guard holding a reference to a mutex. Reverse destruction guarantees that dependencies remain valid while dependent objects execute their destructors."
  • "Where do temporary lifetime bugs typically bite in modern C++?" → "std::string_view and std::span. If you initialize a string_view from a function returning std::string by value, the temporary string destructs at the end of the statement, leaving the view pointing at dead stack or heap memory."
  • "Does returning an object by value hurt performance in low-latency systems?" → "No. Mandatory copy elision in C++17 constructs the return object directly inside the caller's stack frame without copying or moving. Returning references to avoid copies is an obsolete habit that introduces dangling risks."