Skip to content
C++
Library
since C++98
Advanced

Placement new and Manual Object Lifetime

Construct objects in pre-allocated memory using placement new, std::construct_at, std::destroy_at, and std::launder for allocators, pools, and variant storage.

Placement newsince C++98

Placement new constructs an object at a caller-supplied address without allocating memory, separating allocation from initialization and enabling explicit control over object lifetime.

Overview

The familiar expression new T(args) bundles two operations: allocating memory and constructing an object. Placement new breaks that bundle. You supply the memory; the compiler handles only construction. This is the foundation of every custom allocator, arena, memory pool, and in-place container operation in C++.

The standard library itself depends on it. std::vector allocates capacity upfront, then constructs elements into that capacity individually as items are pushed. std::optional and std::variant hold fixed-size byte arrays and use placement new (or its modern wrapper) to construct the contained value. Any implementation of these patterns requires direct lifetime management.

Three mechanisms cover the full lifecycle:

  • Construction: ::new (ptr) T(args) (C++98), or std::construct_at(ptr, args...) (C++20, constexpr-capable)
  • Destruction: ptr->~T() (C++98), or std::destroy_at(ptr) (C++17)
  • Pointer freshness: std::launder(ptr) (C++17) β€” required when reusing storage for objects with const or reference members

Syntax

cpp
// Allocate raw memory β€” correctly sized and aligned
alignas(T) std::byte storage[sizeof(T)];       // stack
void* heap = ::operator new(sizeof(T));        // heap, untyped

// Construct β€” always qualify with :: to bypass class-specific operator new
T* p = ::new (storage) T(arg1, arg2);

// C++20: prefer this β€” works in constexpr contexts
T* p = std::construct_at(reinterpret_cast<T*>(storage), arg1, arg2);

// Use normally
p->method();

// Destroy β€” explicit destructor call or C++17 helper
p->~T();           // direct
std::destroy_at(p); // C++17, identical for non-array T

// Range destruction (C++17)
std::destroy(first, last);
std::destroy_n(first, n);

// Deallocation β€” only if you allocated the storage
::operator delete(heap);
// Stack storage: nothing β€” scope handles it

The :: prefix on ::new (ptr) T(...) is deliberate. Without it, name lookup might find a class-specific operator new overload that allocates new memory β€” the opposite of what placement new is for. Always qualify.

Alignment is non-negotiable. sizeof(T) bytes of char is insufficient if T requires stricter alignment. Use alignas(T) std::byte[sizeof(T)] on the stack. For heap storage, ::operator new(std::size_t, std::align_val_t) (C++17) handles over-aligned types:

cpp
// Over-aligned type (e.g. SIMD vector requiring 32-byte alignment)
void* mem = ::operator new(sizeof(T), std::align_val_t{alignof(T)});  // C++17
T* p = ::new (mem) T();
p->~T();
::operator delete(mem, std::align_val_t{alignof(T)});  // C++17 β€” must match

Examples

Arena allocator with typed construction

cpp
template<typename T, std::size_t N>
class InlinePool {
    alignas(T) std::byte storage_[sizeof(T) * N];
    std::size_t count_ = 0;

public:
    template<typename... Args>
    [[nodiscard]] T* construct(Args&&... args) {
        if (count_ >= N) throw std::bad_alloc{};
        T* slot = reinterpret_cast<T*>(storage_) + count_;
        std::construct_at(slot, std::forward<Args>(args)...);  // C++20
        ++count_;
        return slot;
    }

    void destroy_all() noexcept {
        std::destroy_n(reinterpret_cast<T*>(storage_), count_);  // C++17
        count_ = 0;
    }

    ~InlinePool() { destroy_all(); }
};

// Usage
InlinePool<std::string, 8> pool;
std::string* s = pool.construct("hello, world");
std::cout << *s << '\n';
pool.destroy_all();

Union-like storage with explicit lifetime

cpp
template<typename T>
class ManualStorage {
    alignas(T) std::byte buf_[sizeof(T)];
    bool live_ = false;

public:
    template<typename... Args>
    T& emplace(Args&&... args) {
        if (live_) reset();
        std::construct_at(ptr(), std::forward<Args>(args)...);  // C++20
        live_ = true;
        return *ptr();
    }

    void reset() noexcept {
        if (live_) {
            std::destroy_at(ptr());   // C++17
            live_ = false;
        }
    }

    T& get() noexcept { assert(live_); return *ptr(); }
    bool active() const noexcept { return live_; }
    ~ManualStorage() { reset(); }

private:
    T* ptr() noexcept {
        return std::launder(reinterpret_cast<T*>(buf_));  // C++17
    }
};

Exception safety for heap placement

If the constructor throws in a placement new expression, no destructor is called β€” the object was never fully constructed. The matching operator delete with the same extra arguments is invoked if one exists; the standard placement operator delete(void*, void*) is a deliberate no-op. The caller owns deallocation:

cpp
void* raw = ::operator new(sizeof(Widget));
Widget* w = nullptr;
try {
    w = ::new (raw) Widget(risky_constructor_arg);
    w->run();
    w->~Widget();
    ::operator delete(raw);
} catch (...) {
    // w was never constructed (or fully constructed) β€” do not call destructor
    ::operator delete(raw);
    throw;
}

Wrap raw allocations in RAII to avoid leaks in the error path.

constexpr (C++20)

std::construct_at and std::destroy_at are constexpr in C++20. Raw ::new is not. This is why std::vector became constexpr in C++20 β€” the allocator machinery underneath switched to std::construct_at.

cpp
constexpr int sum_of_squares(int n) {
    std::vector<int> v;           // constexpr vector β€” C++20
    for (int i = 1; i <= n; ++i)
        v.push_back(i * i);
    return std::accumulate(v.begin(), v.end(), 0);
}
static_assert(sum_of_squares(4) == 30);

Best Practices

Prefer std::construct_at / std::destroy_at over raw syntax in new code. They are constexpr-capable and integrate cleanly with std::allocator_traits.

Always use alignas(T) std::byte[sizeof(T)] for in-place storage, not bare char[]. Since C++23, std::aligned_storage is deprecated β€” the manual alignas pattern is the recommended replacement.

Use :: on placement new to prevent accidental dispatch to a class-specific operator new that might allocate.

Wrap raw heap memory in RAII before calling a constructor that can throw. The placement new mechanism will not clean up the allocation on throw.

Call std::destroy_n / std::destroy for ranges rather than looping explicit destructor calls. It is cleaner and becomes constexpr in C++20.

Common Pitfalls

Calling delete p instead of p->~T() is the most common error. delete calls the destructor and then calls operator delete on the pointer. If the storage was separate heap memory, this is a double-free and a mismatched deallocation. If the storage was a stack buffer, it is undefined behavior.

Forgetting the destructor entirely is silent UB for types with non-trivial destructors. For trivially destructible types the omission is harmless but still communicates wrong intent.

Array placement new is unsafe: ::new (buf) T[n] may place a hidden size "cookie" before the first element, shifting the actual array start past buf. Use a loop of individual construct_at calls instead.

Accessing an object after its destructor has run β€” between p->~T() and the next construction in that storage β€” is UB even if you don't free the memory.

Omitting std::launder on reused storage: when you destroy an object and construct a new one of the same type in the same storage, a pointer held over from the first object is technically invalid. If T has any const or reference non-static data members, the compiler is entitled to cache their values and never re-read through the old pointer. std::launder returns a fresh pointer with correct provenance:

cpp
struct Config { const int timeout; };

alignas(Config) std::byte buf[sizeof(Config)];
Config* c1 = ::new (buf) Config{30};
int t1 = c1->timeout;   // 30 β€” fine

c1->~Config();
Config* c2 = ::new (buf) Config{60};

// c1->timeout is UB β€” c1 points to a dead object
// reinterpret_cast<Config*>(buf)->timeout may return 30 (cached)
Config* c3 = std::launder(reinterpret_cast<Config*>(buf));  // C++17
int t3 = c3->timeout;   // guaranteed 60

Copy-assignment by destroy + reconstruct is an anti-pattern. If the copy constructor throws after the destructor has run, the object is left in a destroyed state. Use copy-and-swap or direct member assignment instead.

See Also