C++ gives us precise tools for managing the ownership of objects, but when it comes to the transitions between raw storage and live objects, there’s a lot to be desired.

The state of affairs

This was motivated while working on code that every C++ programmer eventually encounters: small-buffer optimization. The idea is that instead of always heap-allocating, you can reserve some storage inline and, if the object you need to create fits, you can construct it there. The standard library uses this in std::string, std::any, std::function, and std::optional.

Here’s an example, commented to call out some of the caveats:

class AnyDerived {
    static constexpr std::size_t buf_size = 64;

    Base* p_ = nullptr;

    // The alignas is required but easy to forget. Nothing in the
    // language warns you if you omit it, and on x86 the bug will
    // never surface because the CPU handles misaligned accesses.
    // But not all platforms do, and this same code could crash
    // or cause silent data corruption.
    alignas(Base) std::byte buf_[buf_size];

    bool on_heap() const noexcept
    {
        return static_cast<void*>(p_) != static_cast<void*>(buf_);
    }

    template <typename Derived, typename... Args>
    void construct(Args&&... args)
    {
        static_assert(alignof(Derived) == alignof(Base));

        // If the object won't fit into the buffer, just allocate
        // it directly using new:
        if constexpr (sizeof(Derived) > buf_size)
        {
            p_ = new Derived(std::forward<Args>(args)...);
            return;
        }

        // We can construct the object into our buffer, avoiding an
        // existing allocation. We will use placement new: a special
        // form of new that doesn't allocate memory; we provide the
        // address into which the object is to be constructed and new
        // does it for us. The syntax is alien enough that programmers
        // may never encounter it.
        // Also note that this call also looks deceptively simple because
        // the C array silently decays to a pointer, which in turn implicitly
        // converts to the void* parameter that placement new expects. When
        // the raw storage is not a C array, casting may be necessary and
        // conversions become explicit.
        p_ = new (buf_) Derived(std::forward<Args>(args)...);
    }

    void destroy()
    {
        if (on_heap())
        {
            // Plain old delete
            delete p_;
        }
        else
        {
            // Explicit destructor call: the only place in the language
            // where you call a member function whose name starts with ~.
            // How would you even search for this syntax if you didn't
            // already know it existed?
            p_->~Base();
        }

        p_ = nullptr;
    }
};

All this works and has worked for decades. But it requires the programmer to get a lot just right, especially alignment, with minimal help from either the language or the compiler.

C++17 partially addressed the destruction side with std::destroy_at(p): same semantics as the explicit destructor call, but discoverable, searchable, and self-documenting.

And C++20 tried to address the construction side with std::construct_at:

template <typename T, typename... Args>
constexpr T* construct_at(T* p, Args&&... args);

The problem with this interface is that you need to already have a pointer to the type you want to construct that points to the memory to construct the new object into.

So, unlike the placement new syntax, you now definitely need a cast:

std::construct_at(
    reinterpret_cast<Derived*>(buf_),
    std::forward<Args>(args)...);

The cast is not just ceremony: to use this modern C++ API you need to actively bend the type system.

At the root of the problem is that the library has no vocabulary for “storage that is suitable for a T but doesn’t contain one yet.” We have void* (“address of something”), std::byte* (“address of raw bytes”), and T* (“address of a T”).

A modest proposal

We should introduce a type that represents the “ready for construction” state:

namespace std {

template <typename T>
class raw_memory {
    void* addr_;

    static constexpr bool
    is_aligned(void* p) noexcept
    {
        return (reinterpret_cast<std::uintptr_t>(p) % alignof(T)) == 0;
    }

public:
    // From a raw pointer: trust the caller for size and alignment.
    constexpr explicit raw_memory(void* p) noexcept : addr_(p)
    {
        contract_assert(p != nullptr);
        contract_assert(is_aligned(p));
    }

    // From a dynamic span: runtime size check, contract for alignment.
    constexpr explicit raw_memory(std::span<std::byte> s) : addr_(s.data())
    {
        contract_assert(is_aligned(s.data()));
        if (s.size() < sizeof(T))
            throw std::invalid_argument("insufficient storage for T");
    }

    // From a fixed-extent span: compile-time size check.
    template <std::size_t N>
        requires (N >= sizeof(T))
    constexpr explicit raw_memory(std::span<std::byte, N> s) noexcept
        : addr_(s.data())
    {
        contract_assert(is_aligned(s.data()));
    }

    // From a byte array: compile-time size check.
    template <std::size_t N>
        requires (N >= sizeof(T))
    constexpr explicit raw_memory(std::byte (&buf)[N]) noexcept
        : addr_(buf)
    {
        contract_assert(is_aligned(buf));
    }

    constexpr void* get() const noexcept { return addr_; }
};

}  // namespace std

Then extend construct_at to accept it:

namespace std {

template <typename T, typename... Args>
constexpr T* construct_at(raw_memory<T> where, Args&&... args)
{
    return ::new (where.get()) T(std::forward<Args>(args)...);
}

}  // namespace std

The SBO skeleton becomes:

template <typename Derived, typename... Args>
void construct(Args&&... args)
{
    if constexpr (sizeof(Derived) > buf_size)
        p_ = new Derived(std::forward<Args>(args)...);
    else
        p_ = std::construct_at(
            std::raw_memory<Derived>{buf_},
            std::forward<Args>(args)...);
}

void destroy()
{
    if (on_heap())
        delete p_;
    else
        std::destroy_at(p_);

    p_ = nullptr;
}

No placement new, no explicit destructor calls, no need to cast. Every lifecycle transition goes through a named function, and raw_memory<Derived> makes the “uninitialized storage” state visible to both the programmer and the compiler.

It’s worth noting that unlike the reinterpret_cast that std::construct_at would otherwise require, the type is specified through raw_memory<Derived> in a context where the type system is active and can validate, rather than one where it is explicitly bypassed.

A natural home for invariant checks

Beyond simplifying the code, the other benefit of raw_memory<T> is that it gives us a place to put robust checks that can catch bugs, often at compile time.

With raw_memory<T>, we can implement a robust contract to detect common mistakes such as attempting to construct an object into a buffer that is too small or is not properly aligned.

In a hardened implementation, a misaligned address triggers a contract violation at the point where storage is declared suitable for T, before any construction is attempted.

Cost

Ultimately, raw_memory<T> is primarily a wrapper around a pointer.

The size check is resolved entirely at compile time against fixed-extent spans. Against dynamic spans, it requires a single comparison at runtime. The alignment check during construction requires a modulo and a comparison (and a compiler might be able to elide entirely in many cases).

raw_memory<T> is, effectively, a zero-cost abstraction.

A precedent

C++ has done this before. std::unique_ptr was as much a vocabulary type as it was a better smart pointer: the type made ownership visible in the type system, so the compiler and the programmer could agree on who was responsible for deallocation.

std::raw_memory is the same idea applied one level down. It makes the “uninitialized storage” state visible in the type system, so the compiler and the programmer can agree on what operations are valid at any given point in an object’s lifecycle.