Another missing piece: std::raw_memory and lifecycle management
22 Apr 2026
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.