Object Lifetimes — Storage Duration, Placement New, Alignment
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, ordynamic - 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 withdelete - Lifetime: from
newtodelete - Manual control, heap-allocated
Object Lifetime Rules
An object's lifetime begins when:
- Storage with proper size and alignment is obtained
- Its initialization is complete (constructor returns, or for trivial types, the storage is allocated)
An object's lifetime ends when:
- Its destructor is called
- 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
- What are the four storage durations in C++? Give an example of each.
- When does an object's lifetime begin? When does it end?
- Why would you use placement new instead of regular
new? - What is alignment and why does it matter?
- What does
std::launderdo 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
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