Article

Choosing C++ Containers: Hidden Allocations and Reference Lifetimes

C++ standard containers can allocate memory without an explicit new expression. Understanding vector reallocation, deque and list validity guarantees, and span ownership helps connect container choices to storage layout and mutation.

Share

Koharu's reading tip

Do you need to preserve a reference to an element, or an iterator used for traversal? Separating those requirements makes container choices much clearer.

Koharu's reading tip

Searching for new expressions does not reveal every allocation in a C++ program. A container declared as a local variable can acquire separate storage as elements are added.

On October 17, 2026, the ISO C++ Blog highlighted “A quick C++ standard container overview,” which approaches standard containers through these hidden allocations. The practical question goes beyond allocation counts: when elements move, what happens to the references you already hold?

Connecting layout, capacity, and reference lifetimes gives you a reason to choose one container over another. Start by separating the container object from its element storage.

A local vector still acquires separate element storage

A locally declared std::vector<int> has two parts to consider: the vector object and storage for its elements. vector uses an allocator, and the default std::allocator obtains storage through ::operator new. Dynamic allocation can therefore occur without new appearing at the call site. The allocator-aware container requirements and default allocator specification establish this relationship.

By contrast, std::array<int, 3> holds a fixed number of elements within itself. That does not mean an array must live on the stack. If it is a member of a dynamically created object, its elements are part of that object. The array specification and storage duration rules for subobjects explain why a type name alone does not determine storage location.

Dynamically growing containers also organize storage differently. Jens Weller’s linked article describes typical layouts: contiguous storage for vector, blocks for deque, and linked nodes for list. Layout helps explain the mechanism, but code should rely on the validity guarantees specified for each operation.

vector reserve prepares capacity without adding elements

std::vector<T> stores elements contiguously when T is not bool. Appending has amortized constant complexity, while middle insertions and erasures take linear time. That makes vector a useful starting candidate for collecting elements and traversing them in order. vector specification

Growing beyond the available storage requires reallocation. reserve(n) prepares capacity for at least n elements without changing size(). Calling reserve(3) on an empty vector still leaves it empty. Capacity and reserve specification

Assuming each operation succeeds, a small sequence makes the distinction concrete:

Operation Size What is known about capacity
Call reserve(3) on an empty vector 0 At least 3
push_back(10) 1 No insertion-triggered reallocation
Append 20 and 30 3 Still no reallocation
Append 40 4 Reallocation if the previous capacity was 3

reserve(3) does not guarantee a capacity of exactly 3, so the fourth insertion does not necessarily reallocate.

Reallocation invalidates all existing element pointers, references, and iterators. Even without reallocation, an insertion in the middle invalidates those at or after the insertion point. Reserving capacity does not make references stable across every mutation. Insertion and invalidation rules

Choose deque or list around the references and operations you need

When you need to retain references while inserting elements, other containers offer different guarantees. Distinguish referring to an element from retaining an iterator for traversal.

Inserting at either end of a deque preserves references to existing elements, but invalidates iterators. Inserting in the middle invalidates both. A blanket claim that deque handles insertion without invalidation is therefore insufficient. deque insertion rules

For list, insertion preserves existing element references and iterators; erasure invalidates only those referring to erased elements. Constant-time insertion of one element assumes the insertion position is already available. It does not eliminate the work needed to find that position. list modifiers

The distinction also matters for unordered_map. A rehash redistributes elements among buckets and invalidates iterators, but preserves element pointers and references. Unordered associative container requirements

First identify candidates that meet the required guarantees, then compare execution time and memory use. Instead of assuming node-based storage is always slower, measure with comparable element counts and insertion, erasure, and traversal workloads. That reveals the cost of the guarantees your application actually needs.

Borrowing through span does not prevent vector reallocation

Does the consumer need to own the data at all? std::span<T> views a contiguous sequence owned by another object. It can avoid copying elements into another container merely to pass them along, but it does not keep their storage alive. span specification

If a vector reallocates after a span is created from it, accessing its elements through that span is no longer valid. Destroying the owner has the same consequence. Follow the operations on the owner between creating the view and finishing its use.

For example, collecting data first, then passing a span while keeping the vector unchanged until reading finishes, makes the lifetime conditions easier to establish. Handing out views while the vector is still growing requires a different scope of reasoning.

Understanding hidden allocations helps you explain who owns the elements and which operations change storage or invalidate references. Once those conditions are clear, you can decide whether vector capacity management is sufficient, whether another validity guarantee is needed, or whether borrowing through span is enough.

Source

Share

Related Articles

These articles share nearby categories or tags, so you can keep reading along the same thread.