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

construct_at / destroy_at

Low-level object lifetime utilities for constructing and destroying objects in raw memory without invoking operator new or delete.

std::construct_at / std::destroy_atsince C++17/C++20

std::destroy_at (C++17) and std::construct_at (C++20) are low-level utilities in <memory> that separate object lifetime from storage allocation, enabling correct construction and destruction of objects in uninitialized memory β€” including in constant-evaluated contexts.

Overview

When you manage raw memory manually β€” through a custom allocator, a pool, or std::allocator_traits β€” you need to separate two orthogonal concerns: obtaining storage and beginning object lifetime. The standard has provided placement new for construction and explicit destructor calls for destruction since C++98, but neither is usable in constexpr contexts, and both require syntactic ceremony.

C++17 introduced std::destroy_at, std::destroy, and std::destroy_n into <memory> to give named, range-aware interfaces for destruction. C++20 completed the picture with std::construct_at, which mirrors placement new but is constexpr-capable and fits cleanly into std::allocator_traits.

All four functions live in <memory> and are in namespace std.

FunctionSinceconstexpr since
std::destroy_atC++17C++20
std::destroyC++17C++20
std::destroy_nC++17C++20
std::construct_atC++20C++20

Syntax

cpp
#include <memory>

// C++20 β€” construct object at p, forwarding args to constructor
template<class T, class... Args>
constexpr T* std::construct_at(T* p, Args&&... args);

// C++17 β€” destroy object at p (calls destructor, does NOT free memory)
template<class T>
constexpr void std::destroy_at(T* p);  // constexpr since C++20

// C++17 β€” destroy a range [first, last)
template<class ForwardIt>
constexpr void std::destroy(ForwardIt first, ForwardIt last);

// C++17 β€” destroy n objects starting at first
template<class ForwardIt, class Size>
constexpr ForwardIt std::destroy_n(ForwardIt first, Size n);

construct_at(p, args...) is equivalent to ::new(static_cast<void*>(p)) T(std::forward<Args>(args)...) but can appear in constexpr functions where placement new cannot (until compilers begin implementing the C++26 relaxations).

destroy_at(p) calls p->~T(). For arrays, since C++20, it calls destroy on each element in order before the array's lifetime ends.

Examples

Custom pool allocator skeleton

The canonical motivation: a fixed-size memory pool that manages object lifetimes explicitly.

cpp
#include <memory>
#include <array>
#include <cstddef>
#include <new>

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

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

    void destroy(T* p) {
        std::destroy_at(p); // C++17; does NOT release the backing storage
    }
};

Note that destroy_at ends the object's lifetime without touching the memory: the storage remains valid for reuse.

Uninitialized buffer with range construction/destruction

cpp
#include <memory>
#include <string>

void demo() {
    constexpr int N = 4;
    alignas(std::string) std::byte buf[sizeof(std::string) * N];
    std::string* p = reinterpret_cast<std::string*>(buf);

    // Construct four std::string objects in raw storage β€” C++20
    for (int i = 0; i < N; ++i)
        std::construct_at(p + i, "hello");

    // Use objects normally ...

    // Destroy all four with a single call β€” C++17
    std::destroy(p, p + N);
    // Storage buf[] is still alive; no memory freed
}

Constexpr context (C++20)

std::construct_at is indispensable when implementing constexpr-capable containers, since placement new is not allowed in constant expressions:

cpp
#include <memory>

template<typename T>
struct ConstexprStorage {
    alignas(T) char buf[sizeof(T)];

    template<typename... Args>
    constexpr T* emplace(Args&&... args) {
        // placement new here would reject constexpr β€” C++20
        return std::construct_at(
            reinterpret_cast<T*>(buf),
            std::forward<Args>(args)...
        );
    }

    constexpr void reset() {
        std::destroy_at(reinterpret_cast<T*>(buf)); // constexpr since C++20
    }
};

constexpr int test() {
    ConstexprStorage<int> s;
    s.emplace(42);
    int v = *reinterpret_cast<int*>(s.buf);
    s.reset();
    return v; // 42
}
static_assert(test() == 42);

allocator_traits integration

Standard allocator-aware containers delegate to std::allocator_traits, which itself calls std::construct_at under the hood since C++20:

cpp
#include <memory>

template<typename Alloc>
void construct_via_traits(Alloc& alloc,
                          typename std::allocator_traits<Alloc>::pointer p,
                          int val) {
    // allocator_traits::construct calls construct_at if no custom construct()
    std::allocator_traits<Alloc>::construct(alloc, p, val); // C++11+, updated C++20
}

Before C++20, allocator_traits::construct used placement new internally. In C++20 it was updated to use std::construct_at, enabling constexpr allocators.

Destroying an array (C++20 extension to destroy_at)

C++20 extended std::destroy_at to handle array types, calling element destructors in order:

cpp
#include <memory>
#include <string>

void destroy_array() {
    alignas(std::string) std::byte buf[3 * sizeof(std::string)];
    auto* arr = reinterpret_cast<std::string(*)[3]>(buf);

    // Construct array elements individually (construct_at for arrays is C++20)
    std::construct_at(arr, "a", "b", "c"); // aggregate init β€” illustrative

    // In C++20, destroy_at on an array pointer destroys elements in order
    std::destroy_at(arr); // C++20 array overload
}

Best Practices

Pair every construct_at with exactly one destroy_at. These functions manage object lifetime, not storage. Failing to call destroy_at on a non-trivially-destructible type is undefined behavior (the destructor for resources like std::string or std::unique_ptr never runs).

Prefer std::destroy / std::destroy_n for ranges. Writing a manual loop of destroy_at calls is correct but noisier. The range versions also allow the implementation to apply optimisations for trivially-destructible types (the standard permits treating destroy as a no-op on trivially destructible types).

Use construct_at rather than placement new in new code. The two are semantically equivalent in non-constexpr contexts, but construct_at participates in std::allocator_traits protocols, is findable by tooling, and composes naturally with other <memory> utilities.

For pre-C++20 code needing constexpr construction, you will have to live with placement new outside constant expressions and rely on if consteval (C++23) or compiler-specific extensions.

Always ensure adequate alignment. Raw storage for objects of type T must be at least alignof(T)-aligned. Use alignas(T) on std::byte arrays or std::aligned_storage_t (deprecated C++23, but still available). Misaligned construction is undefined behavior regardless of which construction mechanism you use.

Common Pitfalls

Calling destroy_at on an unconstructed slot. Uninitialized storage holds no object; calling destroy_at on it invokes undefined behavior. Track which slots are live, especially in pool implementations where you may reuse slots.

Double-destroy. After destroy_at(p), the object no longer exists. Any further access β€” including a second destroy_at β€” is undefined behavior. This is easiest to trigger in exception-safety code that cleans up partially-constructed ranges.

Forgetting that destroy requires forward iterators pointing to live objects. Passing an iterator range where some elements were never constructed, or were already destroyed, is undefined behavior regardless of whether their types are trivially destructible.

Confusing delete with destroy_at. delete p ends the object's lifetime AND releases its storage. destroy_at(p) ends lifetime only. If the storage was obtained with new, you still need to call operator delete (or the appropriate deallocation function) separately after destroy_at.

construct_at does not zero-initialize trivial types by default. std::construct_at(p) value-initializes T, which zero-initializes scalar types. But std::construct_at(p, args...) with arguments performs direct-initialization β€” be explicit about your intent.

std::construct_at Deep Dive

What the standard actually says

The C++20 specification defines std::construct_at as equivalent to:

cpp
// <memory> β€” C++20
template<class T, class... Args>
constexpr T* construct_at(T* p, Args&&... args) {
    return ::new(static_cast<void*>(p)) T(std::forward<Args>(args)...);
}

The key phrase is "equivalent to" β€” implementations may differ internally, but the effect must match that of a placement new expression. The static_cast<void*> suppresses any class-specific operator new(size_t, void*) overload in favor of the global non-allocating placement form, which is what you always want when constructing into pre-existing storage.

Why constexpr placement new was impossible before C++20

Placement new was explicitly excluded from constant expressions prior to C++20 (see P0784R7, "More constexpr containers"). The reason is subtle: the compiler's constant-expression evaluator tracks object lifetimes in a structured way that requires knowing exactly which storage a new object occupies. A raw void* passed to placement new gives the evaluator no way to verify that the pointer refers to suitable, previously-allocated-in-constexpr storage β€” the check would require pointer provenance tracking the compiler did not perform.

P0784R7 solved this by introducing two cooperative mechanisms:

  1. Compiler-tracked constexpr allocations (via std::allocator<T> returning typed pointers whose provenance is known).
  2. std::construct_at, which the compiler recognizes as a special form and evaluates directly instead of lowering to a placement new instruction.

The net result: std::construct_at is constexpr because the compiler gives it special-case treatment that it refuses to give to user-written placement new expressions. This is one of the rare standard library functions whose constexpr-ness depends on compiler magic rather than the usual rules.

Initialization semantics

construct_at performs direct-initialization from the forwarded arguments:

cpp
#include <memory>

struct Counted {
    int value;
    explicit Counted(int v) : value(v) {}
};

alignas(Counted) std::byte buf[sizeof(Counted)];
auto* p = std::construct_at(reinterpret_cast<Counted*>(buf), 42);
// p->value == 42, direct-initialization used

// Zero-arg form: value-initializes T (zero-initializes scalars, calls default ctor for class types)
alignas(int) std::byte ibuf[sizeof(int)];
auto* ip = std::construct_at(reinterpret_cast<int*>(ibuf));
// *ip == 0 β€” value-initialization guarantees this

This differs from default-initialization (which leaves scalars indeterminate). When you want zero-initialized trivial objects, construct_at(p) with no arguments is correct and constexpr-safe.

Uninitialized Range Algorithms (C++17)

C++17 moved a family of range algorithms from <algorithm> conventions into <memory>, all targeting uninitialized storage. They construct objects in-place without reading the destination β€” reading uninitialized bytes is undefined behavior, and these algorithms carefully avoid it.

Algorithm signatures

cpp
#include <memory>

// Copy-construct from [first, last) into [d_first, ...)
template<class InputIt, class ForwardIt>
ForwardIt uninitialized_copy(InputIt first, InputIt last, ForwardIt d_first);

// Copy-construct n elements from first into [d_first, ...)
template<class InputIt, class Size, class ForwardIt>
ForwardIt uninitialized_copy_n(InputIt first, Size count, ForwardIt d_first);

// Move-construct from [first, last) into [d_first, ...)
template<class InputIt, class ForwardIt>
ForwardIt uninitialized_move(InputIt first, InputIt last, ForwardIt d_first);

// Move-construct n elements from first into [d_first, ...)
template<class InputIt, class Size, class ForwardIt>
std::pair<InputIt, ForwardIt> uninitialized_move_n(InputIt first, Size count, ForwardIt d_first);

// Value-initialize [first, last) β€” scalars zeroed, class types default-constructed
template<class ForwardIt>
void uninitialized_value_construct(ForwardIt first, ForwardIt last);

// Default-initialize [first, last) β€” scalars indeterminate, class types default-constructed
template<class ForwardIt>
void uninitialized_default_construct(ForwardIt first, ForwardIt last);

// Fill [first, last) by copy-constructing from value
template<class ForwardIt, class T>
void uninitialized_fill(ForwardIt first, ForwardIt last, const T& value);

// Fill n elements starting at first by copy-constructing from value
template<class ForwardIt, class Size, class T>
ForwardIt uninitialized_fill_n(ForwardIt first, Size count, const T& value);

Every algorithm in this family requires that the destination range [d_first, d_first + n) contains no live objects β€” the storage must be raw, uninitialized bytes. Passing a range of already-constructed objects is undefined behavior.

Execution policies (C++17 parallel STL)

All of the above algorithms have overloads accepting an execution policy as the first argument:

cpp
#include <execution>
#include <memory>
#include <vector>

std::vector<std::string> src = {"alpha", "beta", "gamma", "delta"};
alignas(std::string) std::byte dest[4 * sizeof(std::string)];
auto* d = reinterpret_cast<std::string*>(dest);

// Parallel copy-construction into raw storage
std::uninitialized_copy(std::execution::par_unseq,
                        src.begin(), src.end(), d);

std::destroy(std::execution::par_unseq, d, d + 4);

The parallel policy is only worthwhile for large ranges of expensively-constructible types. For trivially-constructible types the implementation is allowed to use memcpy/memset regardless of policy, and the overhead of parallelism would dominate.

Practical example: implementing a simple vector buffer

cpp
#include <memory>
#include <stdexcept>
#include <utility>

template<typename T>
class RawBuffer {
    T*          data_;
    std::size_t size_;
    std::size_t cap_;

public:
    explicit RawBuffer(std::size_t cap)
        : data_(static_cast<T*>(::operator new(cap * sizeof(T))))
        , size_(0), cap_(cap) {}

    ~RawBuffer() {
        std::destroy(data_, data_ + size_);  // end lifetimes
        ::operator delete(data_);            // release storage
    }

    void push_back(const T& val) {
        if (size_ == cap_) throw std::bad_alloc{};
        std::construct_at(data_ + size_, val);  // C++20
        ++size_;
    }

    // Move elements from another buffer into fresh storage
    void move_from(RawBuffer& other) {
        std::uninitialized_move(other.data_, other.data_ + other.size_, data_);
        size_ = other.size_;
    }
};

std::destroy and std::destroy_n

std::destroy (C++17) destroys every object in a range without touching the underlying storage. It is strictly equivalent to calling destroy_at on each element:

cpp
#include <memory>

// Conceptual implementation
template<class ForwardIt>
constexpr void destroy(ForwardIt first, ForwardIt last) {
    for (; first != last; ++first)
        std::destroy_at(std::addressof(*first));
}

// destroy_n returns the iterator past the last destroyed element
template<class ForwardIt, class Size>
constexpr ForwardIt destroy_n(ForwardIt first, Size n) {
    for (Size i = 0; i < n; ++i, ++first)
        std::destroy_at(std::addressof(*first));
    return first;
}

For trivially destructible types, the standard permits (but does not require) treating these as no-ops. All major implementations do elide the loop for int, float, raw pointers, and similar types.

When you need destroy vs. delete

cpp
#include <memory>
#include <string>

// WRONG β€” delete ends lifetime AND releases storage
// If storage came from an allocator, this is a double-free:
//   delete p;  // <-- do not do this

// RIGHT β€” separate lifetime end from storage release
void release_pool_slot(std::string* p, void* storage) {
    std::destroy_at(p);     // ~string() runs; storage still valid
    my_pool.deallocate(storage);  // now release storage
}

// Range version β€” clean up N elements before returning storage to allocator
void drain(std::string* arr, std::size_t n, MyAllocator& alloc) {
    std::destroy_n(arr, n);          // destroy all n objects
    alloc.deallocate(arr, n);        // return raw storage
}

The separation is mandatory whenever storage does not come from new. Examples include: stack buffers, mmap-backed memory, shared-memory segments, custom pool allocators, and any buffer returned by std::pmr::memory_resource::allocate.

Exception Safety in Uninitialized Move Algorithms

std::uninitialized_move and std::uninitialized_move_n have a subtle exception-safety property that affects how you write robust containers.

What happens when a move throws

If the move constructor of element k throws during uninitialized_move, the standard mandates that the already-moved elements in the destination [d_first, d_first + k) are destroyed before the exception propagates. Elements in the source range that were successfully moved from are left in their "moved-from" state β€” valid but unspecified. The source element that threw is unchanged (since the move never completed for it).

cpp
#include <memory>
#include <stdexcept>
#include <string>

struct ThrowsOnSecondMove {
    static int count;
    std::string payload;

    ThrowsOnSecondMove(std::string s) : payload(std::move(s)) {}
    ThrowsOnSecondMove(ThrowsOnSecondMove&& o) {
        if (++count == 2) throw std::runtime_error{"move failed"};
        payload = std::move(o.payload);
    }
};
int ThrowsOnSecondMove::count = 0;

// uninitialized_move will destroy the first successfully-moved element
// in the destination before rethrowing the exception from the second

This means uninitialized_move does NOT provide the strong exception guarantee β€” the source range is partially moved-from and cannot be reliably restored to its original state.

Prefer uninitialized_copy when T is copyable

std::uninitialized_copy is strictly safer: if construction of element k throws, elements [d_first, d_first + k) in the destination are destroyed, and the source is left completely unchanged (copies do not modify the source). This enables the strong exception guarantee in container reallocations.

The standard library itself uses this rule in std::vector::reserve and std::vector::push_back: if T's move constructor is not noexcept, the vector copies rather than moves during reallocation (std::move_if_noexcept implements this logic). Prefer the same discipline in your own containers.

cpp
template<typename T>
void safe_reallocate(T* old_buf, T* new_buf, std::size_t n) {
    if constexpr (std::is_nothrow_move_constructible_v<T>) {
        std::uninitialized_move(old_buf, old_buf + n, new_buf);
    } else {
        // Falls back to copy β€” strong guarantee preserved
        std::uninitialized_copy(old_buf, old_buf + n, new_buf);
    }
}

allocator_traits::construct and destroy

For generic, allocator-aware code, never call std::construct_at or std::destroy_at directly. Instead, go through std::allocator_traits:

cpp
#include <memory>

template<typename Alloc>
void alloc_aware_construct(Alloc& a,
                           typename std::allocator_traits<Alloc>::pointer p,
                           int val) {
    // Calls Alloc::construct(a, p, val) if that member exists;
    // otherwise falls back to std::construct_at(p, val) since C++20.
    std::allocator_traits<Alloc>::construct(a, p, val);
}

template<typename Alloc>
void alloc_aware_destroy(Alloc& a,
                         typename std::allocator_traits<Alloc>::pointer p) {
    // Calls Alloc::destroy(a, p) if present;
    // otherwise falls back to std::destroy_at(p).
    std::allocator_traits<Alloc>::destroy(a, p);
}

Why this matters: the uses_allocator protocol

Some types propagate allocators to their members. std::allocator_traits::construct participates in the std::uses_allocator protocol: when constructing a T that is uses_allocator<T, Alloc>, the traits inject the allocator into the constructor call automatically. Calling std::construct_at directly bypasses this protocol entirely.

cpp
#include <memory>
#include <tuple>

// std::tuple uses the uses_allocator protocol.
// allocator_traits::construct will call the tuple's allocator-extended ctor.
// std::construct_at(p, a, b, c) would call the tuple's plain 3-arg ctor
// and silently omit the allocator.

template<typename Alloc, typename... Ts>
void construct_tuple(Alloc& alloc,
                     std::tuple<Ts...>* p,
                     Ts&&... args) {
    // CORRECT β€” allocator is threaded in if tuple members support it
    std::allocator_traits<Alloc>::construct(alloc, p, std::forward<Ts>(args)...);

    // WRONG for allocator-aware members β€” allocator is silently dropped:
    // std::construct_at(p, std::forward<Ts>(args)...);
}

The rule of thumb: write allocator_traits::construct/destroy in any function templated on an allocator type, and std::construct_at/destroy_at only in code that owns the lifetime directly and has no allocator context.

constexpr Allocations in C++20

P0784R7 introduced transient constexpr allocations: storage obtained via std::allocator<T>::allocate inside a constant expression. Combined with std::construct_at, this made constexpr std::vector and constexpr std::string possible in C++20.

The transient allocation rule

Any allocation made during constant evaluation must be freed before the constant expression completes. The compiler tracks live allocations; if the expression returns with any allocation still live, it is ill-formed (a compile-time error, not undefined behavior).

cpp
#include <vector>

// Legal: allocation created and freed within the constexpr context
constexpr std::size_t sum_of_range(std::size_t n) {
    std::vector<int> v;       // allocates in constexpr context
    for (std::size_t i = 0; i < n; ++i)
        v.push_back(static_cast<int>(i));
    std::size_t total = 0;
    for (int x : v) total += x;
    return total;             // vector destructor runs here; allocation freed
}
static_assert(sum_of_range(10) == 45);  // fully evaluated at compile time

// ILLEGAL: allocation escapes the constexpr context
// constexpr std::vector<int>* leak() {
//     return new std::vector<int>{1, 2, 3};  // compile error
// }

How std::construct_at fits in

Inside constexpr std::vector, every call to push_back ultimately invokes std::construct_at on the backing buffer. The compiler evaluates this at compile time, tracking the object lifetime of each element. When the constexpr vector destructor runs, std::destroy_at ends each element's lifetime, and std::allocator<T>::deallocate releases the backing buffer β€” all at compile time with no generated machine code.

cpp
#include <memory>

// Simplified constexpr-capable storage cell
template<typename T>
struct ConstexprCell {
    alignas(T) std::byte storage[sizeof(T)];
    bool live = false;

    template<typename... Args>
    constexpr void emplace(Args&&... args) {
        std::construct_at(reinterpret_cast<T*>(storage),
                          std::forward<Args>(args)...);
        live = true;
    }

    constexpr T& get() { return *reinterpret_cast<T*>(storage); }

    constexpr void reset() {
        if (live) {
            std::destroy_at(reinterpret_cast<T*>(storage));
            live = false;
        }
    }

    constexpr ~ConstexprCell() { reset(); }
};

constexpr int round_trip() {
    ConstexprCell<int> cell;
    cell.emplace(99);
    int v = cell.get();
    cell.reset();
    return v;
}
static_assert(round_trip() == 99);

Note that reinterpret_cast inside constexpr functions has restrictions β€” real implementations use compiler intrinsics or __builtin_bit_cast. The pattern above works at runtime; for fully constexpr operation, std::bit_cast or compiler-specific approaches are needed until C++26 relaxations land.

See Also

  • reference/library/memory/placement-new β€” the underlying mechanism construct_at wraps
  • reference/library/memory/allocators β€” std::allocator and allocator_traits build on these primitives
  • reference/library/memory/memory-management β€” uninitialized memory algorithms (uninitialized_copy, uninitialized_fill, etc.)