Skip to content

Object Lifetimes — Storage Duration, Placement New, Alignment

DodaTech Updated 2026-06-28 8 min read

In this tutorial, you will learn about Object Lifetimes. We cover key concepts, practical examples, and best practices to help you master this topic.

C++ objects have well-defined lifetimes beginning with constructor completion and ending with destructor invocation, controlled by storage duration class and placement new for explicit lifetime management.

What You'll Learn

You will understand the four storage durations (automatic, static, thread-local, dynamic), start and end object lifetimes explicitly with placement new and explicit destructor calls, manage alignment requirements for low-level memory, use std::launder to access objects after placement new, and avoid undefined behavior from lifetime violations.

Why It Matters

Object lifetime rules underpin every C++ program. They determine when constructors and destructors run, when virtual dispatch is safe, and when you can reuse memory. In embedded and systems programming, you often need precise control over object lifetimes — constructing objects in shared memory, reusing buffers, or managing memory-mapped I/O. Violating lifetime rules causes undefined behavior that is notoriously hard to debug.

Learning Path

graph LR
    A["26: Allocators"] --> B["27: Object Lifetimes"]
    B --> C["28: Memory Order"]
    C --> D["29: STL Overview"]
    style A fill:#4a90d9,stroke:#2c5f8a,color:#fff
    style B fill:#4a90d9,stroke:#2c5f8a,color:#fff
    style C fill:#4a90d9,stroke:#2c5f8a,color:#fff
    style D fill:#4a90d9,stroke:#2c5f8a,color:#fff

Storage Duration

#include <iostream>

int global = 10;               // static storage duration

thread_local int tls = 20;     // thread-local storage duration (C++11)

void func() {
    static int staticVar = 30;  // static storage duration (initialized once)
    int automatic = 40;         // automatic storage duration
    
    int* dynamic = new int(50); // dynamic storage duration
    
    std::cout << global << " " << tls << " " << staticVar << " ";
    std::cout << automatic << " " << *dynamic << "\n";
    
    delete dynamic;
}

int main() {
    func();
    func();
}

Automatic Storage Duration

  • Objects declared without static, thread_local, or dynamic
  • Lifetime: from declaration to end of enclosing block
  • Stack-allocated, fast, deterministic

Static Storage Duration

  • Global, namespace scope, class static, function-local static
  • Lifetime: from program start to program end
  • Initialized before main() (or on first call for function statics)

Thread-Local Storage Duration (C++11)

  • One instance per thread
  • Lifetime: from thread creation to thread exit
  • Useful for per-thread caches and RNG state

Dynamic Storage Duration

  • Created with new, destroyed with delete
  • Lifetime: from new to delete
  • Manual control, heap-allocated

Object Lifetime Rules

An object's lifetime begins when:

  1. Storage with proper size and alignment is obtained
  2. Its initialization is complete (constructor returns, or for trivial types, the storage is allocated)

An object's lifetime ends when:

  1. Its destructor is called
  2. The storage is released or reused
#include <iostream>

struct Widget {
    int value;
    Widget(int v) : value(v) { std::cout << "Widget created: " << value << "\n"; }
    ~Widget() { std::cout << "Widget destroyed: " << value << "\n"; }
};

int main() {
    Widget w1(1);  // lifetime starts here
    
    {
        Widget w2(2);  // lifetime starts
    }  // w2 lifetime ends
    
    w1.value = 10;  // w1 is still alive
    
    Widget* pw = new Widget(3);
    delete pw;  // pw lifetime ends
}  // w1 lifetime ends

Placement New

Placement new constructs an object at a specific memory address without allocating storage.

#include <iostream>
#include <new>

struct Point {
    int x, y;
    Point(int a, int b) : x(a), y(b) {
        std::cout << "Point(" << x << "," << y << ") constructed\n";
    }
    ~Point() {
        std::cout << "Point(" << x << "," << y << ") destroyed\n";
    }
};

int main() {
    alignas(Point) char buffer[sizeof(Point)];
    
    // Construct a Point in the buffer
    Point* p = new (buffer) Point(3, 4);
    std::cout << p->x << ", " << p->y << "\n";
    
    // Explicitly destroy
    p->~Point();
    
    // Reuse the same memory for another Point
    Point* p2 = new (buffer) Point(5, 6);
    p2->~Point();
}

Placement new is essential for:

  • Custom allocators and memory pools
  • Shared memory and memory-mapped I/O
  • std::vector (constructs elements in pre-allocated storage)
  • Embedded Systems with fixed memory regions

Alignment

Every object has an alignment requirement: its address must be a multiple of some value.

#include <iostream>
#include <cstdint>

int main() {
    std::cout << "alignof(char): " << alignof(char) << "\n";
    std::cout << "alignof(int): " << alignof(int) << "\n";
    std::cout << "alignof(double): " << alignof(double) << "\n";
    std::cout << "alignof(void*): " << alignof(void*) << "\n";
    
    // alignas specifier overrides alignment
    struct alignas(64) CacheLine {
        int data[16];
    };
    std::cout << "alignof(CacheLine): " << alignof(CacheLine) << "\n";
    
    // Allocate aligned storage
    alignas(64) char alignedBuffer[64];
    std::cout << "alignedBuffer address: " << (void*)alignedBuffer << "\n";
    
    // std::aligned_storage for type-erased aligned storage
    using Storage = std::aligned_storage_t<sizeof(double), alignof(double)>;
    Storage storage;
    double* dp = new (&storage) double(3.14);
    std::cout << *dp << "\n";
    dp->~double();
}

std::launder (C++17)

When you reuse memory with placement new, the compiler may assume the old object's value is still valid. std::launder prevents this assumption.

#include <iostream>
#include <new>

struct X {
    const int n;
    X(int v) : n(v) {}
};

int main() {
    X* p = new X(10);
    
    // Reuse storage for a different X
    p->~X();
    X* q = new (p) X(20);
    
    // Without launder, the compiler may assume *p still has n=10
    std::cout << q->n << "\n";  // OK: 20
    
    // With launder, we explicitly tell the compiler about the new object
    std::cout << std::launder(p)->n << "\n";  // OK: 20
}

Lifetime and Undefined Behavior

#include <iostream>

struct TrivialType {
    int x;
};

struct NonTrivial {
    int x;
    NonTrivial(int v) : x(v) {}
    ~NonTrivial() { std::cout << "dtor\n"; }
};

int main() {
    // Trivial types: lifetime starts when storage is allocated
    TrivialType* t = reinterpret_cast<TrivialType*>(new char[sizeof(TrivialType)]);
    t->x = 5;  // OK: trivial type needs no constructor
    std::cout << t->x << "\n";
    delete[] reinterpret_cast<char*>(t);
    
    // Non-trivial: must use placement new
    char buf[sizeof(NonTrivial)];
    // NonTrivial* n = reinterpret_cast<NonTrivial*>(buf);
    // n->x = 5;  // UB: NonTrivial's lifetime has not started
    
    NonTrivial* n = new (buf) NonTrivial(5);
    std::cout << n->x << "\n";
    n->~NonTrivial();
}

Common Mistakes

Mistake 1: Using Memory Before Object Lifetime Starts

char buf[sizeof(Widget)];
Widget* w = reinterpret_cast<Widget*>(buf);
w->doSomething();  // UB: Widget was not constructed

Mistake 2: Calling Destructor Twice

Widget* w = new Widget();
w->~Widget();
delete w;  // UB: destructor called twice

Mistake 3: Incorrect Alignment for Placement New

char buf[sizeof(double)];  // may not be aligned for double
double* p = new (buf) double(3.14);  // UB if buf is not aligned

Use alignas(alignof(T)) char buf[sizeof(T)].

Mistake 4: Assuming Static Objects are Initialized Before main

The order of initialization of static objects across translation units is undefined. This is the "static initialization order fiasco."

Mistake 5: Using an Object After its Lifetime Has Ended

int* p = new int(5);
delete p;
*p = 10;  // UB: use after free

Mistake 6: Mixing new[] with Placement New for Arrays

Array placement new is complex. Consider using std::aligned_storage and manual per-element construction/destruction instead.

Practice Questions

  1. What are the four storage durations in C++? Give an example of each.
  2. When does an object's lifetime begin? When does it end?
  3. Why would you use placement new instead of regular new?
  4. What is alignment and why does it matter?
  5. What does std::launder do and when is it necessary?

Challenge

Implement a simple fixed-capacity container using placement new that works with non-trivial types. Include push_back, pop_back, clear, and proper lifetime management (construction and destruction). Ensure correct alignment.

FAQ

Can I reuse a memory buffer after destroying the objects in it?

Yes. As long as you properly destroy existing objects before constructing new ones, and the storage is appropriately aligned for the new objects.

What is the 'static initialization order fiasco'?

When static objects in different translation units depend on each other, their initialization order is undefined. Solution: use function-local statics instead of global statics.

Is it safe to call `reinterpret_cast` between unrelated pointer types?

Only if you cast to a char*, unsigned char*, or std::byte* type, or if the types are pointer-interconvertible. Otherwise, strict aliasing violations occur.

What is the purpose of `std::start_lifetime_as` (C++23)?

It starts the lifetime of an object of a given type at a storage location without calling a constructor, useful for deserialization and low-level memory manipulation.

Do trivial types have lifetimes?

Yes, trivial types have lifetimes but they can be started implicitly by allocating storage and writing to it. No constructor call is needed.

How does `std::optional` manage object lifetimes?

optional contains a boolean flag and aligned storage. It uses placement new to construct and explicit destructor calls to destroy, providing manual lifetime control.

Mini Project

Build a type-erased Any container that manages object lifetimes correctly:

#include <iostream>
#include <memory>
#include <new>
#include <typeinfo>

class Any {
private:
    struct Base {
        virtual ~Base() = default;
        virtual Base* clone() const = 0;
        virtual const std::type_info& type() const = 0;
    };
    
    template <typename T>
    struct Derived : Base {
        T value;
        Derived(const T& v) : value(v) {}
        Base* clone() const override { return new Derived(value); }
        const std::type_info& type() const override { return typeid(T); }
    };
    
    Base* ptr_ = nullptr;
    
public:
    Any() = default;
    
    template <typename T>
    Any(const T& value) : ptr_(new Derived<T>(value)) {}
    
    Any(const Any& other) : ptr_(other.ptr_ ? other.ptr_->clone() : nullptr) {}
    
    Any(Any&& other) noexcept : ptr_(other.ptr_) {
        other.ptr_ = nullptr;
    }
    
    ~Any() { delete ptr_; }
    
    template <typename T>
    T* cast() {
        if (ptr_ && ptr_->type() == typeid(T)) {
            return &static_cast<Derived<T>*>(ptr_)->value;
        }
        return nullptr;
    }
};

int main() {
    Any a = 42;
    Any b = std::string("hello");
    Any c = a;
    
    int* p = a.cast<int>();
    std::string* s = b.cast<std::string>();
    
    if (p) std::cout << *p << "\n";
    if (s) std::cout << *s << "\n";
}

What's Next

Object lifetimes define when objects exist. The next lesson covers memory ordering: atomics, memory_order semantics, and fence operations for lock-free synchronization.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro