Skip to content
C++
Library
since C++26
Basic

std::inplace_vector (C++26)

Fixed-capacity vector with inline storage — dynamic size 0..N, no heap allocation, constexpr-compatible, and full std::vector-compatible API.

std::inplace_vector<T, N>since C++26

A sequence container with the full std::vector interface but with a fixed maximum capacity N stored entirely within the object — no heap allocation, ever.

Overview

std::inplace_vector<T, N> (standardized via P0843R14 in C++26) fills the gap between std::array and std::vector. std::array<T, N> has fixed compile-time size and stack storage. std::vector<T> has dynamic size and heap storage. std::inplace_vector<T, N> has dynamic size from 0 to N and stack (inline) storage — you get the ergonomics of push_back and erase without a single allocator call.

When to reach for it:

  • Interrupt service routines or real-time audio/control loops where heap allocation is banned
  • Embedded targets with no OS, no allocator, or strict memory budgets
  • Hot paths where std::vector allocation latency is measurable
  • constexpr contexts where you need a resizable container (std::vector is constexpr since C++20 but carries allocator machinery; inplace_vector is simpler)
  • Small, bounded collections where the max element count is a known invariant (command arguments, event listeners, path components)

capacity() is always N — a static constexpr value. There is no reserve, no shrink_to_fit, no allocator template parameter.

Syntax

cpp
#include <inplace_vector>  // C++26

template<class T, size_t N>
class inplace_vector;

Key members beyond the standard sequence interface:

cpp
// Capacity (all constexpr, always return N)
static constexpr size_type capacity() noexcept;
static constexpr size_type max_size() noexcept;

// Throwing insertion — throws std::bad_alloc if size() == capacity()
reference push_back(const T& x);
reference push_back(T&& x);
template<class... Args>
reference emplace_back(Args&&... args);

// Non-throwing insertion — returns T* on success, nullptr if full (C++26)
T* try_push_back(const T& x) noexcept(/*see below*/);
T* try_push_back(T&& x)      noexcept(/*see below*/);
template<class... Args>
T* try_emplace_back(Args&&... args) noexcept(/*see below*/);

// Unchecked insertion — undefined behaviour if size() == capacity() (C++26)
reference unchecked_push_back(const T& x);
reference unchecked_push_back(T&& x);
template<class... Args>
reference unchecked_emplace_back(Args&&... args);

try_push_back and try_emplace_back are noexcept when the element construction itself is noexcept. They return a pointer to the newly inserted element or nullptr if the container was already full. This is the correct API for real-time and signal-handler contexts.

unchecked_push_back skips the capacity check entirely — use it only after an explicit size() < capacity() guard when you need every last cycle.

Examples

Bounded event queue

cpp
#include <inplace_vector>
#include <string_view>

struct Event { int type; uint32_t payload; };

// Fixed-size event queue that lives on the stack — no allocation in the audio thread
void process_midi_events(std::inplace_vector<Event, 64>& queue) {
    for (const auto& ev : queue) {
        dispatch(ev);
    }
    queue.clear();
}

void enqueue(std::inplace_vector<Event, 64>& q, Event ev) {
    // try_push_back: noexcept, returns nullptr if full
    if (q.try_push_back(ev) == nullptr) {
        log_overflow();  // real-time safe — no allocation, no throw
    }
}

Hot path: manual capacity check + unchecked insertion

cpp
void batch_insert(std::inplace_vector<float, 1024>& buf,
                  const float* src, size_t count) {
    const size_t available = buf.capacity() - buf.size();
    const size_t to_copy   = std::min(count, available);

    // unchecked_push_back: no branch per element after the guard above
    for (size_t i = 0; i < to_copy; ++i)
        buf.unchecked_push_back(src[i]);

    if (to_copy < count)
        handle_overflow(src + to_copy, count - to_copy);
}

constexpr use — compile-time sieve

cpp
constexpr auto first_n_primes(auto n) {
    std::inplace_vector<int, 100> primes;
    for (int candidate = 2; static_cast<int>(primes.size()) < n; ++candidate) {
        bool composite = false;
        for (int p : primes) {
            if (p * p > candidate) break;
            if (candidate % p == 0) { composite = true; break; }
        }
        if (!composite) primes.push_back(candidate);
    }
    return primes;
}

constexpr auto p20 = first_n_primes(20);
static_assert(p20[0] == 2);
static_assert(p20[19] == 71);
static_assert(p20.size() == 20);

std::vector cannot be used here at compile time for returning by value (its constexpr support requires the allocator to clean up within the constant expression); inplace_vector has no such restriction.

Trivially copyable propagation

cpp
struct Vec3 { float x, y, z; };

static_assert(std::is_trivially_copyable_v<Vec3>);
// inplace_vector<T, N> is itself trivially copyable iff T is:
static_assert(std::is_trivially_copyable_v<std::inplace_vector<Vec3, 16>>);

// This means the compiler can memcpy the whole container —
// critical for DMA transfers and inter-core shared memory in embedded systems.

C++26 Range Interface

std::inplace_vector fully implements the C++23/26 range-based sequence container interface. These members accept any std::ranges::input_range whose reference type is compatible with T, composing cleanly with std::views pipelines.

assign_range, append_range, insert_range

cpp
#include <inplace_vector>
#include <vector>
#include <ranges>

int main() {
    std::inplace_vector<int, 16> v;

    // assign_range: replaces all existing elements with the range
    // Invalidates all iterators (equivalent to clear() + range insert)
    std::vector<int> src = {1, 2, 3, 4, 5};
    v.assign_range(src);
    // v == {1, 2, 3, 4, 5}

    // append_range: inserts range elements before end()
    // Only invalidates the past-the-end iterator — safe to hold other iterators
    auto evens = std::views::iota(6, 10)
               | std::views::filter([](int x){ return x % 2 == 0; });
    v.append_range(evens);
    // v == {1, 2, 3, 4, 5, 6, 8}

    // insert_range: inserts range at an arbitrary position
    // Invalidates all iterators at or after the insertion point
    auto it = v.begin() + 2;
    v.insert_range(it, std::views::single(99));
    // v == {1, 2, 99, 3, 4, 5, 6, 8}
}

Note on prepend_range: The standard explicitly does not provide push_front, emplace_front, pop_front, or prepend_range on std::inplace_vector. To prepend a range, use insert_range(v.begin(), rg) — this is O(N) due to element shifting.

Overflow behaviour with range functions: append_range and insert_range throw std::bad_alloc if the range would cause size() to exceed N, matching the single-element push_back contract.

std::span Interop — Zero-Copy View

Because inplace_vector satisfies the ContiguousContainer concept (elements stored at contiguous addresses), std::span's contiguous-range constructor accepts it implicitly. You can pass an inplace_vector to any API that takes a std::span<T> or std::span<const T> without copying or converting.

cpp
#include <inplace_vector>
#include <span>
#include <numeric>

// Some API that only knows about spans
void compute(std::span<const float> data) {
    float sum = std::accumulate(data.begin(), data.end(), 0.f);
    // ...
}

void write_dma(std::span<const uint8_t> bytes);

int main() {
    std::inplace_vector<float, 32> samples;
    samples.append_range(std::views::iota(0, 8)
                       | std::views::transform([](int i){ return float(i) * 0.1f; }));

    // Implicit conversion: inplace_vector<float,32> → span<const float>
    compute(samples);

    // Also works for mutable spans
    std::span<float> mutable_view = samples;
    std::ranges::fill(mutable_view, 0.f);

    // And for byte-level views (e.g. DMA transfer)
    std::inplace_vector<uint8_t, 64> packet;
    packet.assign_range(std::views::iota(uint8_t{0}, uint8_t{8}));
    write_dma(packet);  // span<const uint8_t>, zero copy
}

The implicit conversion goes through std::span's constructor template that accepts any contiguous_range. It does not require an explicit cast or .data() call. Conversely, std::span does not own data — the inplace_vector must outlive any span derived from it.

Migrating from boost::container::static_vector

boost::container::static_vector<T, N> (Boost.Container) was the primary pre-C++26 equivalent. The core container semantics are identical; migration is mostly mechanical.

Header and namespace:

cpp
// Before (Boost)
#include <boost/container/static_vector.hpp>
using boost::container::static_vector;
static_vector<int, 64> v;

// After (C++26)
#include <inplace_vector>
std::inplace_vector<int, 64> v;

API mapping — members that carry over unchanged:

static_vector memberinplace_vector equivalent
push_back / emplace_backidentical — throws std::bad_alloc on overflow
pop_back, insert, erase, clearidentical
at, operator[], front, back, dataidentical
size, empty, max_sizeidentical
begin / end / rbegin / rendidentical

Key differences to watch for:

cpp
// 1. capacity() is now static constexpr — can be used in constant expressions
//    Boost: size_type capacity() const noexcept;  (runtime member)
//    C++26: static constexpr size_type capacity() noexcept;
constexpr auto cap = std::inplace_vector<int, 64>::capacity();  // OK in C++26
// std::array<Desc, MyVec::capacity()> table;  // also valid

// 2. Overflow policy: Boost allows compile-time selection via static_vector_options;
//    std::inplace_vector always throws std::bad_alloc by default,
//    but provides separate try_* and unchecked_* families instead of a policy flag.

// Boost (configurable throw policy):
// using no_throw_sv = static_vector<int, 64,
//     static_vector_options<throw_on_overflow<false>>::type>;

// C++26 (choose per call-site):
std::inplace_vector<int, 64> v;
v.push_back(x);                  // throws on overflow
auto* p = v.try_push_back(x);    // returns nullptr on overflow (noexcept-ish)
v.unchecked_push_back(x);        // UB on overflow — fastest

// 3. Range interface: append_range / assign_range / insert_range are new in C++26;
//    Boost.static_vector has no equivalents.

// 4. Reserve / shrink_to_fit: both exist in std::inplace_vector but are static
//    no-ops (capacity is fixed). Boost behaves the same way.

What does NOT exist in std::inplace_vector:

  • No static_vector_options customization point
  • No push_front, emplace_front, pop_front, or prepend_range
  • No allocator template parameter

Best Practices

Choose try_push_back over push_back in real-time code. push_back throws std::bad_alloc on overflow — even if your system has exceptions enabled, throwing inside a real-time callback is typically illegal. try_push_back is noexcept and lets you handle the full-queue case explicitly.

Check triviality of T to understand copy/move cost. For trivially copyable types, the implementation can memcpy the internal buffer. For types with non-trivial move constructors, every push_back costs a move construction and a counter increment.

Prefer inplace_vector over a raw std::array + size counter. The manual pattern is error-prone and reimplements the wheel. inplace_vector gives you range-based for, iterators, std::ranges compatibility, and std::span interop for free.

Use capacity() as a protocol constant. Because capacity() is static constexpr, you can use it at compile time to size related structures: std::array<Descriptor, MyQueue::capacity()> is valid.

Common Pitfalls

Move is O(N), not O(1). std::vector move is O(1) — it steals a pointer. std::inplace_vector must move each element individually because the storage is inside the object; the source cannot be left empty by a pointer swap. For large N with non-trivially-movable types, this matters.

cpp
std::inplace_vector<std::string, 64> a = /* ... */;
auto b = std::move(a);  // O(N) string moves — not O(1)
// a is valid-but-unspecified state per the standard (elements were moved from)

Overflow throws std::bad_alloc, not std::length_error. The exception type follows the "allocation failed" semantic, not the "exceeded max_size" semantic. Code catching std::length_error to handle overflow will miss it.

cpp
std::inplace_vector<int, 2> v = {1, 2};
try {
    v.push_back(3);
} catch (const std::length_error&) { /* never reached */
} catch (const std::bad_alloc&)    { /* this fires */ }

push_back does not invalidate iterators, but insert and erase do. Because the storage address never changes, a push_back to the back cannot move existing elements. However, insert in the middle shifts elements rightward, so all iterators at or after the insertion point are invalidated — same rule as std::deque.

resize beyond capacity throws. Unlike std::vector::reserve, there is no way to ask inplace_vector to grow its storage. resize(M) with M > N throws std::bad_alloc.

inplace_vector vs std::array vs std::vector

Choosing the right sequence container is a trade-off between ownership, size flexibility, and allocation cost. The table below maps the three canonical choices onto the axes that matter most for performance-sensitive and embedded C++.

Propertystd::array<T, N>std::inplace_vector<T, N>std::vector<T>
Storage locationinline (stack/object)inline (stack/object)heap
Heap allocationneverneveralways (at least once)
Size at runtimefixed = Ndynamic 0 … Ndynamic 0 … unbounded
capacity() typestatic constexprstatic constexprruntime value
Default-initialises all N elementsyesno (only size() elements)no
push_back / emplace_backnoyesyes
std::span implicit conversionyesyesyes
Move complexityO(N) if T non-trivialO(N) if T non-trivialO(1) (pointer swap)
constexpr (C++26)yesyesyes (with allocator caveat)
Trivially copyable when T isyesyesno (pointer member)

std::array is the right choice when every slot is always occupied — it has no notion of "empty" slots and default-initialises all N elements. std::inplace_vector fits when you need a resizable subsequence whose maximum length is a known invariant. std::vector is correct when the upper bound is genuinely unknown or when unbounded growth is expected and heap latency is acceptable.

Exception Safety

Overflow throws std::bad_alloc, making push_back a hard failure when the container is full. This mirrors the allocator contract: if storage cannot be obtained the constructor throws, regardless of whether the storage is on the heap or inline. The container remains in a valid, unchanged state after the throw.

cpp
// C++26
#include <inplace_vector>
#include <stdexcept>

void demonstrate_overflow() {
    std::inplace_vector<int, 2> v;
    v.push_back(1);
    v.push_back(2);  // size() == capacity() == 2

    try {
        v.push_back(3);           // throws std::bad_alloc
    } catch (const std::bad_alloc& e) {
        // v is unchanged: still {1, 2}
        assert(v.size() == 2);
    }
}

For contexts where throwing is illegal — interrupt handlers, real-time audio callbacks, noexcept functions — use try_push_back and try_emplace_back. They return a T* pointing to the newly inserted element, or nullptr when the container is full, and are noexcept when element construction is noexcept.

cpp
// C++26
#include <inplace_vector>

struct Sample { float left, right; };

// Called from a JACK audio callback — must never throw or allocate
bool record_sample(std::inplace_vector<Sample, 512>& buf, Sample s) noexcept {
    // try_emplace_back is noexcept(is_nothrow_constructible_v<Sample, Sample&&>)
    // Sample is trivially constructible, so the whole call is noexcept.
    Sample* inserted = buf.try_emplace_back(s);
    return inserted != nullptr;  // false means buffer full — caller decides
}

The three-tier insertion API lets you match the error-handling strategy to the call site: push_back for ordinary code that propagates exceptions, try_push_back for real-time code that handles overflow inline, and unchecked_push_back for hot paths where a prior bounds check already guarantees space.

inplace_vector with SIMD and Fixed-Size Aligned Storage

Fixed inline storage makes inplace_vector a natural staging buffer for SIMD operations. Because capacity() is a compile-time constant the compiler can see the exact buffer size when auto-vectorising loops, eliminating the dynamic-length uncertainty that prevents vectorisation of std::vector loops in some cases.

cpp
// C++26
#include <inplace_vector>
#include <span>
#include <cstring>

// Accumulate 8 floats at a time using compiler-autovectorised loop.
// The fixed N=256 lets the compiler unroll and vectorise with confidence.
float sum_samples(const std::inplace_vector<float, 256>& buf) {
    float acc = 0.f;
    // Contiguous storage — compiler can emit SIMD load + reduce
    for (float v : buf) acc += v;
    return acc;
}

// For explicit SIMD intrinsics, obtain a raw pointer via data().
// std::span<const float> enforces const-correctness across API boundaries.
void apply_gain(std::inplace_vector<float, 256>& buf, float gain) {
    std::span<float> view = buf;                 // zero-copy, no cast needed
    float* __restrict__ p = view.data();
    const size_t n        = view.size();

    // Hand-rolled SIMD or compiler-vectorisable loop — same pointer, same storage
    for (size_t i = 0; i < n; ++i) p[i] *= gain;
}

When alignment beyond alignof(T) is required (e.g. 32-byte alignment for AVX2 256-bit loads), wrap inplace_vector in a struct with alignas — the inline storage will inherit the alignment of the enclosing object.

cpp
// C++26
#include <inplace_vector>

struct alignas(32) AlignedBuf {
    std::inplace_vector<float, 64> data;
};

// AlignedBuf on the stack: &data[0] is 32-byte aligned, ready for AVX2 loads.

The std::span interop described earlier is the idiomatic way to pass the buffer to SIMD-aware functions without exposing the inplace_vector type — the callee sees a plain contiguous view and can call .data() and .size() directly.

inplace_vector in constexpr Contexts

inplace_vector is fully constexpr in C++26, enabling compile-time algorithms that need bounded dynamic storage. Unlike std::vector, whose constexpr support requires the allocator to clean up within the constant expression evaluation, inplace_vector carries no allocator and its storage is part of the object — the compiler can track it without any heap bookkeeping.

cpp
// C++26
#include <inplace_vector>
#include <algorithm>

// Build a sorted, deduplicated list of enum values at compile time.
enum class Flag : uint8_t { A=1, B=2, C=4, D=8 };

constexpr auto unique_flags(std::initializer_list<Flag> input) {
    std::inplace_vector<Flag, 16> result;
    for (Flag f : input)
        if (std::ranges::find(result, f) == result.end())
            result.push_back(f);
    std::ranges::sort(result, [](Flag a, Flag b){
        return static_cast<uint8_t>(a) < static_cast<uint8_t>(b);
    });
    return result;
}

constexpr auto flags = unique_flags({Flag::C, Flag::A, Flag::B, Flag::A, Flag::C});
static_assert(flags.size() == 3);
static_assert(flags[0] == Flag::A);
static_assert(flags[1] == Flag::B);
static_assert(flags[2] == Flag::C);

Compile-time capacity overflow is also caught at compile time — push_back in a constexpr context throws std::bad_alloc, and since exceptions cannot propagate out of constant evaluation, the compiler rejects the program with a diagnostic rather than silently invoking undefined behaviour.

cpp
// C++26
constexpr auto overflow_example() {
    std::inplace_vector<int, 2> v;
    v.push_back(1);
    v.push_back(2);
    v.push_back(3);  // compile error: constant expression cannot throw std::bad_alloc
    return v;
}
// static_assert(overflow_example().size() == 3);  // would not compile

Because capacity() is static constexpr, you can use it as a template argument or array bound inside the same constant expression — enabling self-referential compile-time data structures where the capacity of one container governs the size of a companion lookup table.

See Also