The C++26 standard features a new container called std::inplace_vector, that sits in between the std::array and std::vector containers. It’s a container with a compiled-time fix capacity – like std::array, but a runtime variable size – like std::vector. In this article, we will discuss how it works and when to use it.
A tale of two containers
Before we discuss std::inplace_vector, let’s briefly look at the other two mentioned containers:
std::arrayis a container with a fixed size and capacity known at compile time. Astd::array<T, N>always holdsNelements, not more, not less. When the array is constructed, all theNelements of it are default constructed, no matter how many elements you actually need. This also implies that the typeTmust be default construcible. Otherwise, you need to use workarounds, such as usingstd::optional<T>.arraystores its elements contiguously in memory.std::vectoris a container with a variable size. Its elements are constructed only when they’re added, but they are allocated on the heap.vectoris also a contiguous storage container.
There are cases were you need the semantics of std::array for a fixed (maximum) number of elements, but mixed with those of std::vector for being able to increase and decrease the capacity. If you use a std::array, you need to use an additional counter that you increment and decrement as elements are added or removed. But decrementing the counter does not destruct the actual element of the array. You also need a sentinel to indicate a no-value. There is a lot of bookkeeping around such a solution. If you use a std::vector, you need to ensure the maximum size is never exceeded, which also implies bookkeeping.
Enter inplace_vector
This is what std::inplace_vector was designed for: bridge the two worlds of compile-time fixed capacity and run-time variable size while keeping the elements contiguous, in-place.
The name might be confusing at first but it makes a lot of sense. dynamic_array, static_vector, or even stack_vector have been proposed or used as alternatives for other implementations of a container with similar semantics. However, neither of these catches the true meaning of this container. The committee opted for inplace_vector because this expresses the best the fact that the elements of this container sit where its owner sits: it can be on the stack, it can be on the heap, or the static storage.
#include <inplace_vector> std::inplace_vector<int, 8> v; // capacity 8, size 0, no heap allocation v.push_back(1); v.push_back(2); v.size(); // 2 v.capacity(); // always 8
std::inplace_vector is also a contiguous storage container. Its elements are only constructed when you add them, so T doesn’t need to be default-constructible. It has essentially the same interface as std::vector: iterators, operator[], insert, erase, emplace_back, resize, append_range, and so on. It does not take an allocator parameter. This is the same idea as boost::container::static_vector and similar “static vector” types that game, embedded, and low-latency codebases have used for years.
What happens when it’s full? Since capacity can’t grow, the interface provides three flavors of insertion:
push_back/emplace_backthat throw if capacity is exceeded.try_push_back/try_emplace_backthat returnnullptron failure and leave the container unchanged.unchecked_push_back/unchecked_emplace_backthat don’t throw but assume the precondition thatsize() < capacity()and violating this precondition is undefined behavior.
std::inplace_vector<int, 2> v{1, 2};
v.push_back(3); // throws std::bad_alloc
auto* p = v.try_push_back(3); // returns nullptr on failure, v unchanged
v.unchecked_push_back(3); // precondition: size() < capacity(); violating it is UB
Use cases
Typically, you can use std::inplace_vector when you know a fixed upper bound on the number of elements and want to avoid heap allocation. That includes embedded or freestanding environments, real-time and low-latency code where allocation is forbidden or unpredictable, small temporary buffers in hot loops, and fixed-size data embedded in other structs or passed across shared memory.
To show a concrete example, let’s consider a 2D grid where each cell can have at most 8 neighbors. Pathfinding code like A* asks for neighbors millions of times per frame, so allocating a std::vector for each call is wasteful, while an array<Cell*, 8> needs a separate count.
#include <inplace_vector>
#include <print>
struct Cell { int x, y; };
constexpr int Width = 5;
constexpr int Height = 4;
std::inplace_vector<Cell, 8> neighbors(Cell c)
{
std::inplace_vector<Cell, 8> result;
for (int dy = -1; dy <= 1; ++dy) {
for (int dx = -1; dx <= 1; ++dx) {
if (dx == 0 && dy == 0) continue;
int nx = c.x + dx;
int ny = c.y + dy;
if (nx >= 0 && nx < Width && ny >= 0 && ny < Height)
result.push_back({nx, ny});
}
}
return result;
}
int main()
{
for (Cell c : {Cell{0, 0}, Cell{4, 1}, Cell{2, 2}}) {
auto ns = neighbors(c);
std::print("({}, {}) has {} neighbors:", c.x, c.y, ns.size());
for (Cell n : ns)
std::print(" ({}, {})", n.x, n.y);
std::println();
}
}
The output of this program is the following:
(0, 0) has 3 neighbors: (1, 0) (0, 1) (1, 1) (4, 1) has 5 neighbors: (3, 0) (4, 0) (3, 1) (3, 2) (4, 2) (2, 2) has 8 neighbors: (1, 1) (2, 1) (3, 1) (1, 2) (3, 2) (1, 3) (2, 3) (3, 3)