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.

std::launder and Pointer Provenance (C++17)

When you destroy an object and placement-new a new one into the same storage, you need a pointer that refers to the new object. The question is: can you reuse the pointer you already have, or do you need a fresh one?

The answer depends on the type. If T has no const non-static data members and no reference non-static data members, the C++ standard permits using an old pointer to the storage to access the new object. The object can be said to "transparently replace" the predecessor; the old pointer is implicitly redirected.

If T has any const or reference non-static data member, the rule changes. The optimizer is allowed to treat const members as immutable after their first read β€” because a const member, by definition, cannot change for the lifetime of the object. When you destroy the old object and create a new one, the compiler does not have to re-read through a pointer it already has, because it is entitled to assume the value seen through that pointer is still the same constant it cached earlier.

std::launder is the fix. Its signature is:

cpp
template<class T>
[[nodiscard]] constexpr T* launder(T* p) noexcept;

It takes a pointer to a byte in live storage, and returns a pointer to the object that currently exists at that address. The returned pointer carries fresh provenance β€” no cached values are carried through it. The function itself does nothing at runtime; its only effect is on the compiler's alias analysis.

cpp
#include <new>

struct Slot {
    const int id;   // const member β€” triggers the launder requirement
    double value;
};

alignas(Slot) std::byte buf[sizeof(Slot)];

Slot* s1 = ::new (buf) Slot{1, 0.0};
s1->value = 3.14;

s1->~Slot();
::new (buf) Slot{2, 6.28};  // new object, same storage

// Wrong: reinterpret_cast<Slot*>(buf)->id may yield 1 (cached)
// Wrong: s1->id is UB β€” s1 points to a destroyed object

Slot* s2 = std::launder(reinterpret_cast<Slot*>(buf));  // correct
// s2->id is guaranteed to be 2
// s2->value is guaranteed to be 6.28

std::launder is also required when accessing a placement-new'd object through a pointer to its storage that was obtained before construction. A void* cast to T* through reinterpret_cast is a pointer to storage, not a pointer to the object. Without launder, accesses through it are UB if the object has const or reference members.

What std::launder does not do: it does not end or extend any object's lifetime, it does not perform any synchronization, and it does not make an invalid pointer valid. If no live object exists at the given address, passing that pointer to std::launder is undefined behavior. It only resolves the provenance ambiguity for an object that already exists and is within its lifetime.

When std::launder is not needed: if T has no const or reference non-static data members, and you obtained the pointer from the placement new expression itself (rather than casting old storage), no laundering is required. std::construct_at returns a properly-provenanced pointer to the new object, so results of construct_at never need laundering.

std::start_lifetime_as (C++23)

Placement new is a construction operation: it runs a constructor. Sometimes you need the opposite β€” a region of bytes already contains the right bit pattern for a type (e.g. freshly received from the network or read from disk), and you want the compiler to treat that memory as a live object of a specific type without running any constructor. This is what std::start_lifetime_as (P0593, C++23) provides.

cpp
#include <memory>

// Signatures (C++23)
template<class T>
T* std::start_lifetime_as(void* p) noexcept;

template<class T>
const T* std::start_lifetime_as(const void* p) noexcept;

template<class T>
T* std::start_lifetime_as_array(void* p, std::size_t n) noexcept;

T must be an implicit-lifetime type β€” a scalar, aggregate, or trivially-copyable class. The function implicitly creates an object of type T at address p and returns a pointer to it. Crucially, no constructor is called and no bytes are modified. The value of the resulting object matches the bit pattern already in storage as if the bytes had been memcpy'd into a fresh T.

The primary use case is binary deserialization. Before C++23, code like the following was technically undefined behavior because no T object had been explicitly started at the buffer address:

cpp
// Pre-C++23: technically UB despite working in practice
alignas(PacketHeader) char raw[sizeof(PacketHeader)];
recv(socket_fd, raw, sizeof(raw), 0);
PacketHeader* hdr = reinterpret_cast<PacketHeader*>(raw);  // UB
hdr->parse();

With std::start_lifetime_as, this is well-defined:

cpp
#include <memory>  // C++23

alignas(PacketHeader) std::byte raw[sizeof(PacketHeader)];
recv(socket_fd, raw, sizeof(raw), 0);

// Starts the lifetime of a PacketHeader over the existing bytes β€” no constructor called
PacketHeader* hdr = std::start_lifetime_as<PacketHeader>(raw);
hdr->parse();  // well-defined

The key distinction from placement new is the absence of construction. Placement new with a trivial type also starts the object's lifetime and produces the same bit pattern, but only because trivial constructors do nothing. std::start_lifetime_as makes this intent explicit without the syntactic overhead of a placement new expression, and it is the correct tool when you do not want any side-effects of even a trivial constructor.

std::start_lifetime_as does not help with non-trivial types. If T has a user-provided constructor that performs setup work, you cannot skip it with this function β€” the type is not an implicit-lifetime type and the call is ill-formed.

Destruction Without Deallocation

Because placement new separates allocation from construction, you also need to separate destruction from deallocation. The mechanism is an explicit destructor call.

cpp
T* p = ::new (storage) T(args);
// ... use p ...
p->~T();            // destroy: runs the destructor, ends T's lifetime
// storage is now raw bytes again β€” no deallocation

std::destroy_at(p) (C++17) is equivalent and preferred in new code because it is constexpr-capable and handles the case where T is an array by calling std::destroy on each element.

When explicit destructor calls are required: any type with a non-trivial destructor that manages resources β€” heap memory, file handles, mutexes β€” must have its destructor called explicitly when using placement new. Omitting it leaks those resources even though the storage itself may be reclaimed by other means.

When they are forbidden: calling a destructor explicitly on an object with automatic storage duration (a stack variable) and then letting the variable go out of scope is undefined behavior. The compiler will call the destructor again at scope exit, and invoking a destructor on an already-destroyed object is UB.

cpp
// WRONG β€” double destruction of a local variable
std::string s = "hello";
s.~basic_string();   // explicit call ends s's lifetime
// s goes out of scope β€” destructor called again: UB

Pseudo-destructor syntax for scalar types: the C++ standard permits destructor-call syntax on any scalar type. p->~int() and p->~T() where T is an alias for a scalar type are both valid and are no-ops at runtime. This exists so that generic code can write ptr->~T() for any T β€” including int, double, or a pointer β€” without needing a specialization:

cpp
template<typename T>
void destroy_one(T* p) {
    p->~T();   // works for class types and scalars alike
}

The call is a no-op for trivially destructible types, but it formally ends the object's lifetime, which matters for subsequent reuse of the storage.

aligned_storage vs alignas (C++23 Deprecation)

Before C++11 gave us alignas, providing correctly-aligned raw storage required either a union trick or the std::aligned_storage type trait:

cpp
// C++11 / C++14 / C++17 β€” works but verbose
typename std::aligned_storage<sizeof(T), alignof(T)>::type storage;
T* p = ::new (&storage) T(args);

// Convenience alias (C++14)
std::aligned_storage_t<sizeof(T), alignof(T)> storage;

The template takes two parameters β€” the size in bytes and the required alignment β€” and provides a member typedef type that is a trivial standard-layout type of at least that size and alignment. In practice the type member is typically an array of unsigned char with an alignment attribute.

std::aligned_storage was deprecated in C++23 for several reasons. The API is verbose and error-prone: you must pass size and alignment separately, and they can be mismatched without a compile-time error. The implementation in some standard libraries provided at least the requested alignment rather than exactly it, leading to subtly different behavior across vendors. And crucially, a simpler, clearer alternative already existed via alignas.

C++23 preferred alternative:

cpp
// Direct β€” recommended for all new code
alignas(T) std::byte storage[sizeof(T)];
T* p = ::new (storage) T(args);
p->~T();

For a type-erased buffer that must accommodate any of several types, compute the maximum size and strictest alignment at compile time:

cpp
template<typename... Ts>
struct TypeErasedStorage {
    static constexpr std::size_t size  = std::max({sizeof(Ts)...});
    static constexpr std::size_t align = std::max({alignof(Ts)...});

    alignas(align) std::byte buf[size];
};

This replaces the older std::aligned_union (also deprecated in C++23) in a form the compiler can validate directly.

The Placement operator new Overload and Why You Must Not delete

The standard defines two reserved overloads of operator new that cannot be replaced:

cpp
[[nodiscard]] void* operator new(std::size_t, void* ptr) noexcept;
[[nodiscard]] void* operator new[](std::size_t, void* ptr) noexcept;

Both simply return ptr unchanged and perform no allocation. The size argument is accepted but ignored. These are the overloads that the compiler calls when it evaluates ::new (ptr) T(args).

The corresponding placement operator delete overloads are also reserved and are deliberate no-ops:

cpp
void operator delete(void*, void*) noexcept;   // does nothing
void operator delete[](void*, void*) noexcept; // does nothing

These exist solely to satisfy the language rule that for every operator new overload there must be a matching operator delete overload reachable for exception-cleanup. If the constructor in a placement new expression throws, the compiler calls the matching operator delete β€” and for the placement forms, that cleanup correctly does nothing (because placement new never allocated anything to free).

Why delete p is wrong for placement-new'd objects: delete p calls the destructor on *p and then calls the single-argument operator delete(void*) β€” the normal deallocation function. If p points into stack storage or an arena, this passes an arbitrary address to free() (or its C++ equivalent), which is undefined behavior. If p points into heap memory you allocated separately with ::operator new, delete p will attempt to free it using the wrong overload and mismatched size or alignment information may corrupt the allocator's internal state.

The correct pattern is always to keep allocation and deallocation separate from construction and destruction:

cpp
void* raw = ::operator new(sizeof(Widget));  // allocate
Widget* w = ::new (raw) Widget(args);        // construct
// ... use w ...
w->~Widget();                                // destroy
::operator delete(raw);                      // deallocate β€” NOT delete w

How the Standard Library Uses Placement New

std::optional, std::variant, and std::vector all rely on placement new (or its modern equivalents) internally. Understanding the pattern clarifies why these features exist.

std::optional<T> stores its contained value in-place within the optional object itself β€” no heap allocation is permitted by the standard. Implementations typically maintain an aligned byte buffer of sizeof(T) bytes and a boolean flag. When the optional transitions from empty to containing a value, the implementation runs placement new (or std::construct_at in C++20 implementations) into that buffer. When the optional is reset or destroyed while engaged, it calls val->T::~T() directly. The result is a nullable value type with no dynamic allocation overhead.

std::variant<Ts...> follows the same principle but must accommodate any of the listed alternative types. Implementations use a buffer sized and aligned for the largest and most-aligned type in the list β€” effectively the TypeErasedStorage pattern above. On emplace<I>(), the previous alternative is destroyed in-place and the new one is constructed into the same buffer. The active-index discriminant tracks which type's destructor to call. This is why std::variant is a zero-allocation abstraction.

std::vector<T> separates capacity from size. When the vector grows its capacity, it allocates a new raw buffer and moves or copies existing elements into it using std::uninitialized_move / std::uninitialized_copy β€” both wrappers around individual placement new operations. The old elements are then destroyed in-place with std::destroy_n. The buffer itself is separately managed by the allocator. push_back and emplace_back use std::allocator_traits<A>::construct, which calls ::new (ptr) T(args) through the allocator β€” the same mechanism every time.

Custom allocator-aware containers follow the same structure. The std::allocator_traits interface provides construct and destroy hooks so that allocators can intercept construction and destruction, while the container controls the lifetime of its raw buffer independently.

See Also