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.

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