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_at handles arrays recursively, destroying each element from index 0 to \(N-1\).
  • Compile-Time Execution: Starting in C++20, std::destroy_at is marked constexpr, allowing dynamic memory allocations and object destructions within constexpr evaluation 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_at internally pairs with std::addressof to 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.