AlgoMaster Logo

std::array

Medium Priority20 min readUpdated June 6, 2026
Listen to this chapter
Unlock Audio

std::array<T, N> is a fixed-size container added in C++11 that wraps a C-style array of N elements of type T and gives it a normal STL interface (begin, end, size, at, and so on). The size is part of the type, the storage lives on the stack (or wherever the array variable lives, with no heap allocation of its own), and there's no decay-to-pointer pitfall that comes with raw C arrays. This chapter covers what std::array is, how to declare and initialise it, the operations it exposes, structured bindings, comparisons with C arrays and std::vector, and when to use it.

Why std::array Exists

C-style arrays have three long-running problems that std::array was designed to fix:

  • They decay to pointers. When you pass int prices[5] to a function, the parameter is actually int* and the size information vanishes. sizeof on the parameter inside the function tells you the size of a pointer, not the size of the array.
  • They don't carry their size. You have to pass it as a separate argument, or use a sentinel value, or hope the caller and callee agreed on a constant.
  • They aren't STL-compatible. You can call std::sort on a raw array using pointer iterators, but you can't pass one to a function that takes a generic container with begin() and size() members.

std::array<T, N> keeps the layout of a raw array (contiguous, fixed-size, stack-allocated when declared as a local) but adds the same member functions every other STL container exposes. The size is encoded in the type, so it doesn't vanish at function boundaries, and there's no decay to a pointer when you pass it around.

Here is the same data stored four different ways. Notice what changes and what doesn't.

The element access syntax is identical across all three. The size query is where things diverge: the raw array needs the sizeof divide trick, and that trick only works in the scope where the array was declared. std::array and std::vector both expose size() as a normal member call, and the call works no matter where the container has travelled.

std::array<T, N> has zero space overhead compared with T[N]. The compiler typically lays it out exactly the same way: N contiguous Ts, no length field, no heap pointer. The size information lives in the type, not in the object.

Size Is Part of the Type

This is the single biggest design choice in std::array, and it shapes everything else. A std::array<int, 5> and a std::array<int, 6> are different types. They are not assignable to each other, you can't store them in the same container without erasure, and a function that takes one cannot accept the other.

The trade-off is direct. By making N part of the type, you get a compile-time constant for the size, which means no runtime length check on iteration, no heap allocation, and no possibility of accidentally treating a 5-element array as a 6-element one. The cost is that you can't write a function that accepts arrays of any size as the same parameter. Either you template on N, or you take iterators, or you use a different container.

The function is templated on the size, so the compiler stamps out a separate version for each N you call it with. That's the idiomatic way to write generic code over std::array. If you want a version that works on arrays of any size without per-size instantiations, you'd usually take iterators (a pair of const double*) or a std::span<const double> (C++20).

Aggregate Initialisation: One Brace or Two

std::array is an aggregate, meaning the compiler initialises it by copying values into its underlying array member rather than calling a user-defined constructor. That has a small but visible syntax quirk depending on the C++ standard.

In C++11 and C++14, the formally correct syntax uses double braces, because std::array wraps an internal raw array and you're initialising that nested array:

In C++17 and later, brace elision lets you drop the inner pair, and a single brace is accepted:

In practice, most compilers accept the single-brace form even on -std=c++14 with a relaxed warning, but if you're targeting strict pre-C++17 you should use the double-brace form. Throughout this course we target C++17, so we'll stick to single braces.

If you provide fewer initialisers than the array size, the remaining elements are value-initialised (zero for arithmetic types).

If you provide no initialiser at all and the array is a local variable, the elements have indeterminate values for built-in types. Reading them before assigning is undefined behaviour, exactly as for a raw C array.

The empty-brace form {} value-initialises every element to zero. For built-in types, that's the safe default to use whenever you don't have meaningful initial values.

The API: Access, Iteration, and Bulk Operations

std::array exposes the standard sequence-container interface, restricted to what makes sense for a fixed-size container (no push_back, no resize, no insert, no erase).

OperationWhat it doesCost
operator[](i)Returns the element at index i, no bounds checkO(1)
at(i)Returns the element at index i, throws std::out_of_range on bad indexO(1)
front()Returns the first elementO(1)
back()Returns the last elementO(1)
data()Returns a pointer to the underlying arrayO(1)
size()Returns N (a compile-time constant)O(1)
empty()Returns true if N == 0O(1)
fill(value)Sets every element to valueO(N)
swap(other)Swaps contents with another array of the same typeO(N)
begin, end, cbegin, cend, rbegin, rendIteratorsO(1)

operator[] does not check bounds. Reading or writing past the end is undefined behaviour, and the compiler is free to assume you didn't.

at(i) throws std::out_of_range on a bad index. Use it when you want a checked access, accept the cost of the comparison, and don't mind the exception cost when it does throw.

The exact message depends on the standard library implementation (the example above is libstdc++; libc++ formats it differently), but the type thrown is always std::out_of_range.

operator[] is one instruction (a pointer offset). at adds a comparison and a possible branch to throw. For tight inner loops the difference can show up; for normal application code it's negligible. Use at for any indexed access where the index isn't already known-good.

data() returns a T* to the underlying storage. That pointer is what you pass to a C API that wants a raw pointer plus a length.

data() plus size() is the bridge to any C API that accepts a raw pointer and a length. You get the pointer interop of a C array without losing the size information.

swap(other) exchanges contents with another std::array of the same type. Because the size is part of the type, you cannot swap a std::array<int, 5> with a std::array<int, 6>.

Notice this is O(N): for an array of N elements, the implementation typically does an element-wise swap. That's different from std::vector::swap, which only swaps a few internal pointers and runs in O(1).

Iteration and Algorithms

Because std::array has begin() and end(), every STL algorithm that takes iterators works on it directly.

std::sort works because std::array provides random-access iterators (the same category as std::vector::iterator and raw pointers). std::accumulate and std::count_if only need input iterators, which forward iterators trivially satisfy.

Range-based for is the cleanest way to iterate when you don't need indices.

The reference-capturing form double& lets the loop modify elements in place. Use const double& (or just double for cheap types) when you only want to read.

Structured Bindings

C++17 added structured bindings, which let you give names to the elements of a fixed-size object in one line. std::array is a natural target because its size is known at compile time.

The names width, height, and fps are new local variables. The form auto [a, b, c] = arr copies the elements; auto& [a, b, c] = arr makes them references into the array; const auto& [a, b, c] = arr is read-only references.

Modifying cables modifies the underlying element because the binding is by reference. This pattern is the cleanest way to "unpack" a small fixed-size array of named fields.

Structured bindings also work on tuples and pairs, and on any aggregate type with public data members. Those uses are covered in the _std::pair & std::tuple_ lesson. The relevant point for this chapter is that std::array participates in the same mechanism, which makes it a comfortable return type for functions that produce a small fixed set of values.

The function returns the array by value. The compiler typically uses NRVO (named return value optimisation) so no copy actually happens; the storage is constructed directly in the caller's frame. The caller then binds names to the elements with one line.

std::array vs C Array vs std::vector

A side-by-side comparison helps you pick the right type for a given job.

PropertyC array T[N]std::array<T, N>std::vector<T>
Size known at compile timeYesYesNo
Size carried with the objectNo (decays to pointer)Yes (part of the type)Yes (stored in object)
Storage locationWherever declaredWherever declaredHeap (for elements)
Heap allocationNoneNoneOne on construction/resize
Decays to pointer on useYesNoNo
STL interface (begin, end, size)Via free functionsMember functionsMember functions
Bounds-checked accessNoneat(i)at(i)
Can resizeNoNoYes
Can be returned by value cleanlyNoYesYes
Bit-for-bit copyableYes (with memcpy for trivial types)Yes (compiler-generated copy)No (deep copy via constructor)
Comparable with ==, <Element pointers comparedElement-wise comparisonElement-wise comparison

Translating this into rules of thumb:

  • Fixed size, known at compile time, small to medium count. Use std::array. Stack-allocated, no overhead, full STL interface.
  • Fixed size, but very large (megabytes). Use std::vector with reserve(N) and resize(N). Storing megabytes on the stack risks overflowing it. std::array<char, 4 * 1024 * 1024> declared as a local will likely crash on a default thread stack.
  • Size known only at runtime, or needs to grow. Use std::vector. That's exactly what it's for.
  • You need raw C interop and own the lifetime carefully. A C array can still be a suitable choice for a tiny embedded codebase or a struct that must match a wire format. For anything else, std::array matches the layout while preserving size information.

The cost story is also worth pinning down. A std::array<int, 100> and int[100] occupy the same 400 bytes (assuming 4-byte int). A std::vector<int> with size() == 100 typically occupies three pointer-sized fields on the stack (begin, end, capacity end) plus a heap allocation of at least 400 bytes. The vector pays for the indirection and the allocation in exchange for the ability to grow.

Declaring a std::array<T, N> as a local variable allocates sizeof(T) * N bytes on the stack. For large N or large T, this can overflow the default thread stack (typically 1 MB on Windows, 8 MB on Linux). Use std::vector for sizes in the megabytes.

When to Use std::array

A few common patterns make std::array the obvious choice:

  • Fixed-size lookup tables. Precomputed prime tables, character class lookup, day-of-week names. The size is a compile-time constant, and the table doesn't need to grow.
  • Small fixed buffers. A 16-byte hash output, a 64-byte network packet header, a 32-character display buffer. std::array<std::uint8_t, 16> is the right way to spell "exactly 16 bytes".
  • Coordinates and small geometric data. std::array<double, 3> for a 3D point, std::array<int, 2> for an (x, y) pair when you don't want the named-field overhead of std::pair.
  • Returning multiple values when a `std::tuple` feels heavy. When the values are all the same type, std::array<T, N> with structured bindings reads more cleanly than a tuple.
  • Replacing C-style arrays in modern code. Anywhere you'd write int prices[5];, write std::array<int, 5> prices; instead. You get the STL interface and lose the decay-to-pointer trap at function boundaries.

Here is one realistic example: a small e-commerce inventory snapshot for the five categories the store carries. The snapshot is a fixed-size record that gets passed around and printed.

The InventorySnapshot struct holds two parallel arrays. Both have the same fixed length, which is enforced at compile time, so there's no risk of one growing out of step with the other. The structured binding at the bottom names the five stock counts and lets the alert check home directly. None of this allocates on the heap.

Passing a std::array<T, N> by value copies all N elements. For anything more than a handful of cheap types, pass by const reference (const std::array<T, N>&) just like you would for std::vector.

Comparison and Assignment

std::array supports the relational operators (==, !=, <, <=, >, >=) when the element type does. Comparisons are element-wise.

The < operator does lexicographic comparison: it walks both arrays from index 0, returning the result of the first mismatching pair. This matches the behaviour of std::vector and the standard string types.

Assignment copies elements one by one. Two arrays of the same type are always assignable; two arrays of different types (different N) are not.

Copy is O(N). For arrays of trivially-copyable types (like int or double), the compiler typically emits a memcpy and the cost is the same as a raw-array copy.

std::array<T, 0>: The Zero-Size Case

A std::array<T, 0> is legal and behaves like an empty container: size() returns 0, empty() returns true, and begin() == end(). Calling front(), back(), or operator[] on a zero-size array is undefined behaviour, just like reading past the end of any other array. The case occasionally shows up in template metaprogramming when you stamp out an array whose size comes from a computation that might be zero.

The data() member can return a non-null pointer or null depending on the implementation; portable code shouldn't dereference it for a zero-size array.

Quiz

array Quiz

10 quizzes