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++20std::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.
| Function | Since | constexpr since |
|---|---|---|
std::destroy_at | C++17 | C++20 |
std::destroy | C++17 | C++20 |
std::destroy_n | C++17 | C++20 |
std::construct_at | C++20 | C++20 |
Syntax
#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.
#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
#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:
#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:
#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:
#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.
See Also
reference/library/memory/placement-newβ the underlying mechanismconstruct_atwrapsreference/library/memory/allocatorsβstd::allocatorandallocator_traitsbuild on these primitivesreference/library/memory/memory-managementβ uninitialized memory algorithms (uninitialized_copy,uninitialized_fill, etc.)