Skip to content
C++
Library
since C++98
Advanced

Uninitialized Memory Algorithms

Low-level algorithms for constructing, copying, moving, and destroying objects in raw allocated storage, decoupling allocation from object lifetime management.

Uninitialized Memory Algorithmssince C++98

A family of algorithms in <memory> that use placement new and explicit destructor calls to construct, copy, move, fill, and destroy objects in raw pre-allocated storage, separating memory acquisition from object lifetime management.

Overview

Standard containers decouple two concerns: obtaining raw bytes from an allocator, and constructing objects inside those bytes. The uninitialized memory algorithms are the primitives that handle the second phase. They operate on memory that holds no live objects, invoking constructors via placement new internally and providing well-defined exception semantics.

All functions live in <memory> and group into five families:

FamilyFunctionsIntroduced
Copy into raw memoryuninitialized_copy, uninitialized_copy_nC++98 / C++11
Fill raw memoryuninitialized_fill, uninitialized_fill_nC++98
Move into raw memoryuninitialized_move, uninitialized_move_nC++17
Construct in placeuninitialized_default_construct[_n], uninitialized_value_construct[_n]C++17
Destroydestroy_at, destroy, destroy_nC++17

C++17 added parallel execution policy overloads across most of these. C++20 added std::ranges:: counterparts with range-based signatures and structured return types.

Exception safety. Copy and fill algorithms provide the strong guarantee: if a constructor throws partway through the range, all already-constructed objects in the output are destroyed before the exception propagates. Move variants (uninitialized_move, uninitialized_move_n) clean up the partially-constructed destination but leave moved-from source objects in a valid-but-unspecified state β€” moves are expected not to throw but are not required to.

Syntax

cpp
#include <memory>

// C++98 β€” copy [first, last) into uninitialized storage starting at d_first
template<class InputIt, class ForwardIt>
ForwardIt uninitialized_copy(InputIt first, InputIt last, ForwardIt d_first);

// C++11 β€” copy exactly count elements
template<class InputIt, class Size, class ForwardIt>
ForwardIt uninitialized_copy_n(InputIt first, Size count, ForwardIt d_first);

// C++98 β€” fill [first, last) with copies of value
template<class ForwardIt, class T>
void uninitialized_fill(ForwardIt first, ForwardIt last, const T& value);

// C++98 β€” fill count elements with copies of value
template<class ForwardIt, class Size, class T>
ForwardIt uninitialized_fill_n(ForwardIt first, Size count, const T& value);

// C++17 β€” move [first, last) into uninitialized storage
template<class InputIt, class ForwardIt>
ForwardIt uninitialized_move(InputIt first, InputIt last, ForwardIt d_first);

// C++17 β€” move exactly count elements; returns {in, out} past-the-end pair
template<class InputIt, class Size, class ForwardIt>
std::pair<InputIt, ForwardIt>
uninitialized_move_n(InputIt first, Size count, ForwardIt d_first);

// C++17 β€” default-initialize a range (scalars: indeterminate; class types: default ctor)
template<class ForwardIt>
void uninitialized_default_construct(ForwardIt first, ForwardIt last);

template<class ForwardIt, class Size>
ForwardIt uninitialized_default_construct_n(ForwardIt first, Size count);

// C++17 β€” value-initialize a range (scalars zeroed; class types: default ctor)
template<class ForwardIt>
void uninitialized_value_construct(ForwardIt first, ForwardIt last);

template<class ForwardIt, class Size>
ForwardIt uninitialized_value_construct_n(ForwardIt first, Size count);

// C++17 β€” call destructor of the object at p; handles arrays correctly since C++20
template<class T>
void destroy_at(T* p);

// C++17 β€” destroy [first, last)
template<class ForwardIt>
void destroy(ForwardIt first, ForwardIt last);

// C++17 β€” destroy count objects; returns iterator past the last destroyed
template<class ForwardIt, class Size>
ForwardIt destroy_n(ForwardIt first, Size count);

Every C++17 function also accepts an execution policy as the first argument:

cpp
// C++17 parallel overload
std::uninitialized_move(std::execution::par_unseq, first, last, d_first);

C++20 ranges counterparts accept range arguments and return {in, out} result objects:

cpp
// C++20
auto [in, out] = std::ranges::uninitialized_copy(source_range, dest_span);

Examples

Custom vector β€” copy construction and reallocation

This is the canonical application: a vector-like container that manages its own storage. The three high-water marks (data_, avail_, limit_) track allocated capacity separately from constructed element count.

cpp
#include <memory>
#include <type_traits>
#include <algorithm>

template<typename T>
class SimpleVec {
    T*     data_  = nullptr;
    T*     avail_ = nullptr;   // one-past last constructed element
    T*     limit_ = nullptr;   // one-past last allocated byte
    std::allocator<T> alloc_;

public:
    SimpleVec() = default;

    explicit SimpleVec(std::size_t n) {
        data_  = alloc_.allocate(n);
        limit_ = data_ + n;
        // value-construct: scalars zeroed, class types default-initialized  (C++17)
        avail_ = std::uninitialized_value_construct_n(data_, n);
    }

    SimpleVec(const SimpleVec& other) {
        const std::size_t n = other.size();
        data_  = alloc_.allocate(n);
        limit_ = data_ + n;
        // strong guarantee: if any copy-ctor throws, constructed elements are destroyed  (C++98)
        avail_ = std::uninitialized_copy(other.data_, other.avail_, data_);
    }

    ~SimpleVec() {
        std::destroy(data_, avail_);          // call destructors  (C++17)
        alloc_.deallocate(data_, capacity());
    }

    std::size_t size()     const { return static_cast<std::size_t>(avail_ - data_); }
    std::size_t capacity() const { return static_cast<std::size_t>(limit_ - data_); }

    void push_back(const T& v) {
        if (avail_ == limit_) grow();
        std::construct_at(avail_, v);  // C++20; use ::new(static_cast<void*>(avail_)) T(v) pre-C++20
        ++avail_;
    }

private:
    void grow() {
        const std::size_t new_cap  = std::max<std::size_t>(1, capacity() * 2);
        T*                new_data = alloc_.allocate(new_cap);
        T*                new_avail;

        // Prefer moves when they cannot throw β€” same policy as std::vector  (C++17 if constexpr)
        if constexpr (std::is_nothrow_move_constructible_v<T>) {
            new_avail = std::uninitialized_move(data_, avail_, new_data);  // C++17
        } else {
            new_avail = std::uninitialized_copy(data_, avail_, new_data);  // C++98
        }

        std::destroy(data_, avail_);
        alloc_.deallocate(data_, capacity());

        data_  = new_data;
        avail_ = new_avail;
        limit_ = data_ + new_cap;
    }
};

Writing into a caller-provided raw buffer

Shared memory segments, memory-mapped I/O, and arena allocators hand you raw bytes. uninitialized_copy_n lets you construct objects there without a separate allocation step:

cpp
#include <memory>
#include <span>

void populate_ring_buffer(void* raw, std::size_t bytes,
                          std::span<const Widget> src) {
    const std::size_t max_elems = bytes / sizeof(Widget);
    const std::size_t count     = std::min(src.size(), max_elems);

    Widget* buf = static_cast<Widget*>(raw);
    std::uninitialized_copy_n(src.begin(), count, buf);  // C++11
    // buf[0..count-1] are live objects; caller is responsible for destroy + dealloc
}

Default vs. value construction

For scalar types the two C++17 construct-in-place variants produce different results:

cpp
#include <memory>

alignas(int) std::byte storage[sizeof(int) * 16];
int* p = reinterpret_cast<int*>(storage);

// Default-construct: ints are indeterminate β€” fast but reading them is UB  (C++17)
std::uninitialized_default_construct_n(p, 16);

// Value-construct: ints are zero-initialized β€” safe to read  (C++17)
std::uninitialized_value_construct_n(p, 16);

std::destroy_n(p, 16);  // C++17 β€” still required even for trivially-destructible types
                         // (no-op for int, but required by the interface contract)

For class types both forms invoke the default constructor; the distinction only matters for scalars and POD aggregates.

Best Practices

Track avail_ separately from limit_. The high-water mark of constructed elements must be maintained accurately throughout any operation that can throw. Every exception handler needs it to call std::destroy on exactly the right range β€” not the full allocated capacity.

Prefer uninitialized_move during reallocation when is_nothrow_move_constructible_v<T> is true. Moving avoids deep copies. When the move constructor cannot throw, the operation is still strongly exception-safe. Branch on the type trait at compile time as shown above β€” this is precisely what std::vector implementations do.

Use std::destroy_at for single-object teardown. It is cleaner than spelling out p->~T() and correctly handles array types in C++20.

Migrate to std::ranges:: versions (C++20) in new code. They accept range arguments, return structured {in, out} iterator pairs, and enforce concept constraints on their arguments, which produces better error messages when types are incompatible.

Match allocate with deallocate even when no objects were constructed. If uninitialized_copy throws before constructing any element, the library destroys what it built, but you still own the raw memory from allocate. Always release it in the exception path.

Common Pitfalls

Double destruction after a throwing construct. When uninitialized_copy throws after constructing k elements, the library destroys those k elements internally. If your catch block then calls std::destroy over the full intended range, you destroy already-dead objects β€” undefined behavior. Rely on the high-water mark to know what is actually live.

Reading indeterminate values after uninitialized_default_construct. For scalar types, default construction leaves the value indeterminate. Any read, including one through a pointer or reference, is undefined behavior regardless of what the bits happen to contain. Use uninitialized_value_construct when zero-initialization is required.

std::fill vs. uninitialized_fill confusion. std::fill calls the copy-assignment operator on already-live objects. uninitialized_fill calls the copy constructor on raw memory. Passing an uninitialized range to std::fill causes it to read the left-hand side before assigning, which is undefined behavior.

Wrong header. These algorithms are in <memory>, not <algorithm>. Projects that include <algorithm> but not <memory> will see confusing linker or compile errors.

Object lifetime and reinterpret_cast. Casting a void* or std::byte* buffer to T* before constructing a T there violates the object lifetime rules prior to C++23. Pre-C++20, use std::allocator<T>::allocate which returns correctly-typed storage, or use placement new and capture the returned pointer. C++20 std::construct_at starts object lifetime implicitly; C++23 std::start_lifetime_as handles existing byte arrays explicitly.

std::uninitialized_move and std::uninitialized_move_n (C++17)

Claim. std::uninitialized_move is the move-construction counterpart of std::uninitialized_copy, and it is the primitive that makes vector reallocation efficient for move-only or expensive-to-copy types.

The key behavioral difference from the copy variant is exception safety: if a move constructor throws partway through the range, the already-moved-from source objects cannot be restored to their original state. The destination range is cleaned up (already-constructed destination objects are destroyed), but the source range is left in a valid-but-unspecified state β€” there is no rollback. This is why std::vector only uses uninitialized_move when std::is_nothrow_move_constructible_v<T> is true, and falls back to uninitialized_copy otherwise.

cpp
// C++17
#include <memory>
#include <type_traits>

template<typename T>
T* reallocate(T* old_data, std::size_t old_size, std::size_t new_cap,
              std::allocator<T>& alloc) {
    T* new_data = alloc.allocate(new_cap);
    try {
        if constexpr (std::is_nothrow_move_constructible_v<T>) {
            // Safe to move: source cannot be partially left in a broken state
            std::uninitialized_move(old_data, old_data + old_size, new_data); // C++17
        } else {
            // Fall back to copy for strong exception guarantee
            std::uninitialized_copy(old_data, old_data + old_size, new_data); // C++98
        }
    } catch (...) {
        alloc.deallocate(new_data, new_cap);
        throw;
    }
    std::destroy(old_data, old_data + old_size); // C++17
    alloc.deallocate(old_data, old_size);
    return new_data;
}

std::uninitialized_move_n is the counted variant; it returns a std::pair<InputIt, ForwardIt> holding past-the-end iterators for both source and destination, which is useful when you need to continue operating on the destination range immediately after the move.

cpp
// C++17
#include <memory>

void move_first_n(Widget* src, std::size_t n, Widget* dst) {
    auto [src_end, dst_end] =
        std::uninitialized_move_n(src, n, dst); // C++17
    // src_end == src + n, dst_end == dst + n
    // Objects in [src, src_end) are valid-but-unspecified; must still be destroyed
    std::destroy_n(src, n);                     // C++17
}

std::construct_at and std::destroy_at (C++20)

Claim. std::construct_at is the constexpr-friendly replacement for placement new, and std::destroy_at is the symmetric explicit-destructor-call wrapper β€” together they form the C++20 lifetime-management primitives usable in both runtime and compile-time contexts.

Prior to C++20, placement new (::new(static_cast<void*>(ptr)) T(args...)) was not usable in constexpr functions, making it impossible to implement optional-like or variant-like types that are fully constexpr. std::construct_at(ptr, args...) fills that gap: the compiler can evaluate it at compile time, while at runtime it is equivalent to placement new. std::destroy_at(ptr) replaces the explicit destructor call idiom ptr->~T() and also correctly handles arrays (destroying each element) since C++20.

cpp
// C++20
#include <memory>

template<typename T>
struct ConstexprOptional {
    alignas(T) std::byte storage_[sizeof(T)];
    bool has_value_ = false;

    constexpr T* ptr() noexcept {
        return reinterpret_cast<T*>(storage_); // ok inside consteval context via construct_at
    }

    constexpr void emplace(const T& v) {
        if (has_value_) std::destroy_at(ptr());          // C++20 β€” constexpr-safe
        std::construct_at(ptr(), v);                      // C++20 β€” constexpr placement new
        has_value_ = true;
    }

    constexpr ~ConstexprOptional() {
        if (has_value_) std::destroy_at(ptr());           // C++20
    }
};

// Verifiable at compile time
consteval int test() {
    ConstexprOptional<int> o;
    o.emplace(42);
    return *o.ptr();
}
static_assert(test() == 42);

std::destroy_at handles arrays transparently: std::destroy_at(ptr_to_array) destroys every element of the array in reverse order, which mirrors the destruction order in automatic storage.

Manual Vector Implementation Using Uninitialized Memory

Claim. A SmallVec<T, N> with an inline storage arena demonstrates the full lifecycle of uninitialized memory management β€” allocation, construction, access, and destruction β€” without a heap allocation for small sizes.

The arena is declared as alignas(T) unsigned char storage_[sizeof(T) * N], which gives correctly-aligned raw bytes with no live objects. std::construct_at constructs elements into individual slots and std::destroy tears them down at the end of the object's lifetime. This pattern is the foundation of std::inplace_vector (C++26) and common SSO (small-string optimization) implementations.

cpp
// C++20
#include <memory>
#include <cstddef>
#include <cassert>

template<typename T, std::size_t N>
class SmallVec {
    alignas(T) unsigned char storage_[sizeof(T) * N]; // raw uninitialized bytes
    std::size_t size_ = 0;

    T* slot(std::size_t i) noexcept {
        return reinterpret_cast<T*>(storage_) + i;
    }
    const T* slot(std::size_t i) const noexcept {
        return reinterpret_cast<const T*>(storage_) + i;
    }

public:
    SmallVec() = default;

    // No copies for simplicity; a real implementation would uninitialized_copy
    SmallVec(const SmallVec&) = delete;
    SmallVec& operator=(const SmallVec&) = delete;

    ~SmallVec() {
        // Destroy only the live range [slot(0), slot(size_))  (C++17)
        std::destroy(slot(0), slot(size_));
    }

    void push_back(const T& v) {
        assert(size_ < N && "SmallVec capacity exceeded");
        std::construct_at(slot(size_), v);  // C++20 β€” constructs in raw storage
        ++size_;
    }

    void push_back(T&& v) {
        assert(size_ < N && "SmallVec capacity exceeded");
        std::construct_at(slot(size_), std::move(v)); // C++20
        ++size_;
    }

    void pop_back() {
        assert(size_ > 0);
        std::destroy_at(slot(size_ - 1)); // C++20 β€” explicit destructor call
        --size_;
    }

    T&       operator[](std::size_t i)       noexcept { return *slot(i); }
    const T& operator[](std::size_t i) const noexcept { return *slot(i); }
    std::size_t size()     const noexcept { return size_; }
    bool        empty()    const noexcept { return size_ == 0; }
};

The alignas(T) specifier is mandatory: without it the storage byte array may be misaligned for T, causing undefined behavior on architectures that require natural alignment and performance penalties or faults elsewhere.

std::uninitialized_default_construct vs std::uninitialized_value_construct

Claim. The distinction between uninitialized_default_construct and uninitialized_value_construct matters only for scalar and POD types, but the performance difference can be significant for large buffers, and the correctness difference is critical for security-sensitive code.

For class types with a user-provided default constructor, both forms call that constructor β€” the behaviour is identical. For scalar types and POD aggregates, uninitialized_default_construct leaves values indeterminate (reading them is UB), while uninitialized_value_construct zero-initializes them. Choose based on whether the buffer will be overwritten before it is read (default, fast) or whether the initial zero state is an invariant (value, safe).

cpp
// C++17
#include <memory>
#include <cstddef>

void demo_construct_variants(std::size_t n) {
    std::allocator<int> alloc;
    int* p = alloc.allocate(n);

    // Fast path: buffer will be completely overwritten by a subsequent read loop
    // Indeterminate ints β€” fast, no memset  (C++17)
    std::uninitialized_default_construct_n(p, n);
    for (std::size_t i = 0; i < n; ++i) p[i] = static_cast<int>(i); // overwrite before read: ok

    std::destroy_n(p, n); // C++17

    // Security path: buffer contents must never contain prior heap data
    // Zero-initialized ints β€” equivalent to memset(p, 0, n*sizeof(int))  (C++17)
    std::uninitialized_value_construct_n(p, n);
    // p[0..n-1] are all 0; safe to inspect immediately

    std::destroy_n(p, n); // C++17
    alloc.deallocate(p, n);
}

The following table summarises all six construct-in-place variants and their effect on different type categories:

FunctionScalar/PODClass with default ctorNotes
uninitialized_default_construct[_n]IndeterminateDefault ctorReading scalar result is UB
uninitialized_value_construct[_n]Zero-initializedDefault ctorSafe for scalars
uninitialized_fill[_n]Copy of valueCopy ctor from valueRequires a fill value
uninitialized_copy[_n]Copy from sourceCopy ctorCopies from existing range
uninitialized_move[_n]Move from sourceMove ctorC++17; see exception caveat
construct_at(ptr, args...)Direct-initDirect-initC++20; constexpr-safe

Destroying Ranges: std::destroy and std::destroy_n

Claim. std::destroy and std::destroy_n are the range-level counterparts of std::destroy_at, and their primary use case is teardown of object ranges in custom allocators and containers before returning raw memory to a pool.

For trivially destructible types (all scalars, POD structs with no user destructor), the standard guarantees that calling std::destroy is a no-op and optimising compilers will eliminate the loop entirely β€” there is no cost to being explicit. For non-trivial types, the loop calls each object's destructor in order. A custom allocator's deallocate step must never be reached with live objects in the memory it is freeing; std::destroy is the correct way to drain the range before returning the block to the pool.

cpp
// C++17
#include <memory>

template<typename T>
struct PoolAllocator {
    T*          pool_;
    std::size_t capacity_;
    std::size_t in_use_ = 0;

    // ... pool setup omitted for brevity ...

    // Destroy n live objects starting at ptr, then mark the slots as free
    void deallocate_range(T* ptr, std::size_t n) {
        // Step 1: call destructors on all live objects in the range  (C++17)
        std::destroy_n(ptr, n);  // no-op for trivially destructible T; mandatory for others

        // Step 2: return raw memory to the pool β€” objects are gone, bytes are reclaimed
        in_use_ -= n;
    }

    // Destroy a known [first, last) range of live objects
    void release(T* first, T* last) {
        std::destroy(first, last);          // C++17 β€” iterates and calls each destructor
        in_use_ -= static_cast<std::size_t>(last - first);
    }
};

std::destroy_n returns a past-the-end iterator (one past the last destroyed object), which is useful when chaining operations: you can pass the returned iterator directly to a subsequent std::construct_at loop that refills the same slots with new objects.

Interaction with std::allocator_traits

Claim. The uninitialized memory algorithms bypass the allocator entirely β€” they do not call std::allocator_traits<A>::construct β€” which means allocator-aware types that rely on construct for their initialization protocol must not be managed with these algorithms.

std::allocator_traits<A>::construct(alloc, ptr, args...) is the customization point that allocator-aware containers (like std::vector and std::map) use when constructing elements. An allocator can override construct to inject additional behavior β€” for instance, propagating an allocator into a nested container via the std::uses_allocator protocol. std::uninitialized_copy and friends call placement new directly, completely bypassing this customization point.

cpp
// C++20
#include <memory>
#include <scoped_allocator>

// An allocator-aware inner type that receives its allocator through uses_allocator protocol
struct Inner {
    using allocator_type = std::allocator<int>;
    explicit Inner(std::allocator_arg_t, const allocator_type& a) : alloc_(a) {}
    explicit Inner(int v) : value_(v) {}
    int value_ = 0;
    allocator_type alloc_;
};

void demonstrate_construct_vs_uninitialized() {
    std::allocator<Inner> alloc;
    Inner* storage = alloc.allocate(4);

    // WRONG for allocator-aware types: bypasses allocator_traits::construct
    // Inner's allocator is never propagated  (C++98 β€” does not use allocator protocol)
    std::uninitialized_fill_n(storage, 4, Inner{0});

    std::destroy_n(storage, 4);

    // CORRECT for allocator-aware types: goes through allocator_traits::construct
    // which invokes the uses_allocator two-arg constructor if available  (C++20)
    for (std::size_t i = 0; i < 4; ++i) {
        std::allocator_traits<std::allocator<Inner>>::construct(
            alloc, storage + i, 0); // construct(alloc, ptr, args...)
    }

    // allocator_traits::destroy mirrors construct on teardown  (C++17)
    for (std::size_t i = 0; i < 4; ++i) {
        std::allocator_traits<std::allocator<Inner>>::destroy(alloc, storage + i);
    }

    alloc.deallocate(storage, 4);
}

std::construct_at(ptr, args...) is also not allocator-aware β€” it is purely a constexpr placement-new wrapper. When writing a generic container that must support allocator-aware element types, always route construction and destruction through std::allocator_traits<A>::construct and std::allocator_traits<A>::destroy rather than through either std::construct_at or the uninitialized algorithms directly.

See Also

  • std::allocator β€” standard allocator providing the raw storage these algorithms populate
  • std::construct_at (C++20) β€” type-safe placement-new wrapper that participates in constant evaluation
  • std::start_lifetime_as (C++23) β€” explicitly starts an object's lifetime within a byte buffer without construction
  • std::allocator_traits β€” the customization point for allocator-aware containers to invoke construct/destroy