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.

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.

See Also