How std::destroy_at Destroys Objects in C++
This article explains how the std::destroy_at function,
introduced in C++17 and expanded in C++20, enables developers to
manually invoke an object's destructor without releasing the underlying
memory storage. It details the mechanics of explicit object destruction,
how std::destroy_at differs from the standard
delete operator, why it is essential for low-level memory
management patterns like custom allocators, and how it handles generic
types and arrays safely.
The Distinction Between Destruction and Deallocation
In C++, destroying an object via the standard delete
expression performs two distinct actions sequentially: it invokes the
object's destructor to release any owned resources, and it invokes the
memory deallocation function (such as operator delete) to
return the raw memory to the operating system or heap manager.
Low-level systems and generic containers (like
std::vector) frequently decouple storage allocation from
object lifetime. They allocate raw, uninitialized memory buffers first,
then construct objects inside that memory using placement
new or std::construct_at. When an element is
removed or replaced, only the object's destructor must run—the backing
memory must remain allocated for subsequent operations.
Mechanics of std::destroy_at
Defined in the <memory> header,
std::destroy_at accepts a pointer to an object and ends its
lifetime by invoking its destructor directly, leaving the memory
intact.
Its behavior is functionally equivalent to:
template <class T>
constexpr void destroy_at(T* p) {
if constexpr (std::is_array_v<T>) {
for (auto& elem : *p) {
std::destroy_at(std::addressof(elem));
}
} else {
p->~T();
}
}When called on an object p of a non-array type
T, it executes the explicit destructor call
p->~T(). If T has a trivial destructor
(such as built-in primitive types like int or
float), the operation is a no-op that gets completely
optimized away by the compiler.
Why Use std::destroy_at Over Explicit Destructor Calls
Before std::destroy_at, manual destruction required
writing p->~T(). While functional for non-array types,
this approach presents several limitations that
std::destroy_at resolves:
- Support for Array Types: Directly calling
p->~T()on an array type is ill-formed and causes a compilation error.std::destroy_athandles arrays recursively, destroying each element from index 0 to \(N-1\). - Compile-Time Execution: Starting in C++20,
std::destroy_atis markedconstexpr, allowing dynamic memory allocations and object destructions withinconstexprevaluation contexts. - Generic Safety: In template libraries, invoking
member functions directly can lead to syntax issues or unexpected
behavior when interacting with scalar types or types with overloaded
address-of operators (
operator&).std::destroy_atinternally pairs withstd::addressofto prevent operator-overloading ambiguities.
Practical Example
The following example demonstrates managing an object's lifetime
inside raw, stack-allocated storage using std::construct_at
and std::destroy_at:
#include <iostream>
#include <memory>
#include <string>
int main() {
// Allocate raw, uninitialized storage aligned for std::string
alignas(std::string) std::byte storage[sizeof(std::string)];
// Cast storage to the target type pointer
auto* ptr = reinterpret_cast<std::string*>(storage);
// Construct the string in place
std::construct_at(ptr, "Resource Management with std::destroy_at");
std::cout << *ptr << '\n';
// Destroy the string object; deallocates the internal string buffer
// but leaves the local 'storage' buffer untouched
std::destroy_at(ptr);
// The raw storage can now safely be reused or allowed to fall out of scope
return 0;
}Through this mechanism, std::destroy_at standardizes
manual lifetime termination, ensuring that resource cleanup remains
type-safe, generic-friendly, and completely decoupled from storage
deallocation.