Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Lifetimes, and the compiler that sees them

Storage durations in one breath

C++ classifies every object by its storage duration, which determines when memory is allocated and reclaimed. The three categories cover nearly all cases:

  • Automatic objects are created when control enters their block and destroyed on exit. Their lifetime matches the block’s scope.
  • Static objects exist for the program’s lifetime, created before main and destroyed after it returns.
  • Dynamic objects live on the heap, managed by containers or smart pointers that acquire and release the memory.

The contrast with C is sharp. In C, malloc allocates heap memory without an automatic scope link. The programmer must call free at the exact moment, separating pointer and memory lifetimes. C++ eliminates this gap: containers or smart pointers own heap objects, and those owners have automatic or static lifetimes, so reasoning focuses on scope and ownership rather than matching allocation and deallocation.

RAII links storage duration to resource lifetime. An automatic object that owns a resource releases it in its destructor when the block ends, so the resource’s lifetime matches the object’s lifetime. This enables the compiler to understand and enforce lifetimes.

The dangling taxonomy

The Core Guidelines Lifetime profile enumerates four ways a program can keep using a value after the value’s storage is gone. Each case has a fixed shape, and each can be caught by static analysis when the analysis runs.

  1. Return of a local reference or pointer. A function returns the address of a variable that goes out of scope when the function returns. The returned handle points at memory the compiler is about to reuse.
  2. Use after end of scope or delete. Code reaches through a handle after the object’s destructor has run, or after delete has freed the memory. The handle is live but the value is dead.
  3. Views that outlive their source. A std::string_view, an iterator, or a raw pointer keeps being used after the object it observes dies or moves. The view borrows storage that no longer holds the expected value.
  4. Container invalidation. Inserting or erasing elements can trigger reallocation, which moves the underlying storage. Any pointer, reference, or iterator taken before the operation now points at freed or stale memory.

These four cases are not arbitrary. They are the only ways a handle can survive its referent, because a handle can only outlive its referent through a return, through a delayed use, through a non-owning view, or through a container resize. The compiler can model each shape because each shape is a small, local pattern in the code.

Lifetime-safety analysis as a compiler feature

The analysis is the Clang implementation of the Core Guidelines Lifetime Safety profile. The profile was written to make the rules above checkable, and the Clang work turned those rules into a dataflow pass over the abstract syntax tree. The stable flag is -Wlifetime-safety. On the pinned toolchain, Clang 22.1.8, that stable flag does not exist yet. The experimental form -Xclang -fexperimental-lifetime-safety is available, and it emits only the -Wreturn-stack-address warning. The deeper checks for cases two, three, and four remain silent in this build.

The book treats the analysis as a law the compiler will enforce when the feature matures. The same committee member who advanced the flag in Chapter 01 is behind the wider effort. The point of teaching the taxonomy now is that the discipline is correct regardless of whether the tool shouts at you. A program that obeys the lifetime rules is correct on today’s compiler and will stay correct on tomorrow’s.

Demo: return of a stack address

// warning: address of stack memory associated with local variable 'x' returned [-Wreturn-stack-address]
#include <cstddef>

int* dangling_pointer() {
    int x = 42;
    return &x; // returns address of a local variable
}

int main() {
    int* p = dangling_pointer();
    // Using *p here would be undefined behavior
    (void)p;
    return 0;
}

The warning at the top of the file is the diagnostic Clang emits when a function returns a reference to a local variable. The local lives in the function’s stack frame. When the function returns, that frame is reclaimed, and the returned reference points at storage the next call is free to overwrite. No runtime check can save this case. The value is gone before anyone reads it. The fix is to return by value, or to have the caller pass the destination in and let the function fill it through a reference the caller owns.

Demo: vector invalidation

#include <vector>
#include <iostream>

int main() {
    std::vector<int> v{1, 2, 3};
    int* p = &v[0]; // capture pointer to first element
    v.push_back(4); // can cause reallocation, invalidating p
    // If compiled with AddressSanitizer, a heap-use-after-free error would be reported when *p is accessed.
    std::cout << *p << "\n"; // undefined behavior if reallocation occurred
    return 0;
}

The program captures a pointer to the first element, then calls push_back. A std::vector keeps its elements in a contiguous block. When that block is full, push_back allocates a larger block and moves every element into it. The old block is freed. The captured pointer still points at the old block, so reading through it is a use of freed memory. When compiled with AddressSanitizer the runtime reports heap-use-after-free.

The lesson is general. Any operation that can grow a std::vector can invalidate pointers, references, and iterators into it. The safe pattern is to take the address after the last growth, to call reserve up front when the final size is known, or to keep using the vector’s own indexing instead of a captured handle.

Demo: dangling string_view

#include <string_view>
#include <string>

std::string_view dangling_view() {
    std::string temp = "temporary"; // lives only inside function
    return std::string_view{temp}; // view refers to destroyed string
}

int main() {
    auto sv = dangling_view();
    // Using sv here is undefined behavior because the source string has been destroyed.
    (void)sv;
    return 0;
}

The function returns a std::string_view that refers to a temporary std::string. The temporary is destroyed at the end of the full expression that created it, while the view is returned to the caller. The caller then reads through a view whose characters are already gone. This is undefined behavior, and it is silent. The code compiles and can even appear to work until the memory is reused.

The trap is that std::string_view is cheap to return, so it invites returning a view into a value the function just made. The rule is to return a std::string when the source is a temporary, and to return a std::string_view only when the caller already owns the backing storage for the whole time the view is used.

Demo: lifetime‑bound annotation

#include <string_view>
#include <string>

// First version: no lifetime annotation. The analysis does not warn when a temporary is passed.
std::string_view first_word(const std::string& s) {
    // Return view to first word (up to first space)
    auto pos = s.find(' ');
    if (pos == std::string::npos) return std::string_view{s};
    return std::string_view{s.data(), pos};
}

int main() {
    // Passing a temporary string – the view dangles, but current clang gives no diagnostic.
    auto sv = first_word(std::string{"hello world"});
    (void)sv;
    return 0;
}

/*
// Fixed version with lifetimebound annotation (Clang accepts the attribute).
std::string_view first_word(const std::string& s) [[clang::lifetimebound]] {
    auto pos = s.find(' ');
    if (pos == std::string::npos) return std::string_view{s};
    return std::string_view{s.data(), pos};
}
*/

The first version returns a view into its parameter without any annotation, so the compiler does not warn when a temporary is passed. Adding [[clang::lifetimebound]] to the parameter tells the analysis that the returned view is tied to the argument’s lifetime. Current Clang accepts the attribute but emits no diagnostic. The programmer must still respect the contract, which future compilers will enforce.

The attribute can also be placed on the function itself, applying the rule to every return path, and on constructors: a constructor that stores a pointer or reference member must mark the source parameter lifetimebound so the member’s lifetime cannot outlive the argument. This links the member to the argument and enables static checks.

The pattern appears wherever a function hands back a handle into memory it was given. Accessors that return a reference or a view to a member, parsers that return a view into their input, and span factories all need the attribute to connect output to input.

Views and spans are lifetime-transparent

std::string_view and std::span are non-owning handles. They do not own storage. They merely borrow it. A view has no destructor that frees anything, because there is nothing for it to free. Its correctness depends entirely on the owner staying alive.

Keep the owner alive for at least as long as any view or span that refers to it. A view is a loan. The lender must outlive the loan. Creating the owner and view in the same expression makes the temporary owner die before the view is used. Bind the view to a named owner that lives in an enclosing scope to avoid the hazard.

std::span generalizes the idea from characters to any T. A std::span<const int> is a non-owning window over a row of int values. The same lifetime rule applies. The std::vector<int> or array that backs the span must outlive every use of the span. Because a span is so light, functions must accept spans instead of a raw pointer plus a length. That change makes the non-owning intent explicit and removes a whole class of length mismatch bugs.

gsl::not_null<T>

The guideline support library provides gsl::not_null<T>. It wraps a raw pointer and guarantees that the pointer is never null. The wrapper adds a contract that the analysis can check. In practice, references already enforce non-nullness, so the book uses gsl::not_null only when a raw pointer is required for legacy interop. The lifetime rules still apply on top of the nullness rule. A gsl::not_null that points at a destroyed object is just as dangling as a plain pointer.

The two contracts are independent. gsl::not_null answers the question of whether the pointer is null, while the lifetime rules answer the question of whether the referent is still alive. A handle can satisfy both and still dangle, so the wrapper does not remove the need for the lifetime discipline taught in this chapter.

Try this

Write a function std::span<const int> middle(std::vector<int>& v) that returns a span over the elements v[1] through v[n-2]. State the rule that the caller must keep v alive for the span’s lifetime. Explain what happens if v is a temporary, and show the [[clang::lifetimebound]] marking that makes the contract explicit.