std::array
Fixed-size contiguous array wrapper with value semantics, bounds checking, and full STL compatibility.
std::arraysince C++11A statically-sized contiguous array container with stack storage, value semantics, optional bounds checking via at(), and full STL algorithm integration.
Overview
std::array<T, N> wraps a C-style array T[N] with type safety and composability while maintaining zero-overhead abstraction. It allocates N * sizeof(T) bytes on the stack, knows its own size, doesn't decay to a pointer, and integrates seamlessly with STL algorithms and structured bindings.
Key characteristics:
- Storage: Stack-allocated; size is a compile-time constant
- Value semantics: Full copy and move support (unlike raw arrays which decay to pointers)
- Size: Immutable and
constexpr; known at compile time - Bounds checking:
operator[]is unchecked;at()throwsstd::out_of_range - No overhead: Memory layout identical to
T[N], zero runtime cost versus raw arrays - STL integration: Works with all range algorithms and iterators
Syntax
#include <array>
// Declaration and initialization
std::array<T, N> arr; // uninitialized (for primitives)
std::array<T, N> arr{}; // zero-initialized
std::array<T, N> arr = {v0, v1, v2}; // aggregate initialization
// C++17: CTAD β deduction guide
std::array arr{1, 2, 3}; // deduces std::array<int, 3> // C++17
// C++20: std::to_array helper
auto arr = std::to_array({1, 2, 3}); // deduces std::array<int, 3> // C++20
// Key member functions (all C++11+)
arr.size(); // constexpr std::size_t
arr.max_size(); // same as size()
arr.empty(); // constexpr; false if N > 0
arr.data(); // T* to underlying buffer
arr.fill(value); // assigns to all elements
arr.begin(), arr.end(); // iterators (const variants available)
arr.at(i); // bounds-checked access
arr[i]; // unchecked accessExamples
Construction and initialization
#include <array>
// Aggregate initialization (C++11)
std::array<int, 3> a = {1, 2, 3};
std::array<int, 3> b{4, 5, 6};
// Default and zero-initialization
std::array<int, 5> raw; // uninitialized β garbage
std::array<int, 5> clean{}; // zero-initialized β all 0
// Fill and copy/move (C++11)
std::array<double, 4> filled;
filled.fill(3.14);
auto copy = a; // copy constructor
auto moved = std::move(a); // move constructor (still O(N) for primitives)Access and bounds checking
std::array<int, 5> arr = {10, 20, 30, 40, 50};
// Unchecked access β no bounds checking
int x = arr[2]; // 30
// Bounds-checked access β throws std::out_of_range
try {
int y = arr.at(10);
} catch (const std::out_of_range& e) {
std::cerr << "Index out of range\n";
}
// Direct pointer to underlying data
int* ptr = arr.data();
// Tuple-like access by compile-time index (C++11)
int first = std::get<0>(arr); // alternative to arr[0]
// Structured bindings (C++17)
auto [x, y, z] = std::array{1, 2, 3};Iteration and algorithms
#include <array>
#include <algorithm>
std::array<int, 5> arr = {5, 3, 1, 4, 2};
// Range-based for (C++11)
for (int n : arr) {
std::cout << n << ' ';
}
// STL algorithms β legacy style (C++11)
std::sort(arr.begin(), arr.end());
std::find(arr.begin(), arr.end(), 3);
// Range algorithms (C++20)
std::ranges::sort(arr);
std::ranges::reverse(arr);
auto it = std::ranges::find(arr, 3);
// Element-wise and lexicographic comparisons (C++11)
std::array<int, 3> a1 = {1, 2, 3};
std::array<int, 3> a2 = {1, 2, 3};
bool equal = (a1 == a2); // true
bool less = (a1 < a2); // falseGeneric functions and std::span
// Accept std::array of any size (C++11)
template<size_t N>
void process(const std::array<int, N>& arr) {
// N is known at compile time
}
// Accept any contiguous range β more flexible (C++20)
void process(std::span<const int> data) {
// works with std::array, std::vector, C arrays, pointers
for (const auto& elem : data) {
// process elem
}
}
// Size-agnostic iteration
template<typename T, size_t N>
void print(const std::array<T, N>& arr) {
for (const auto& elem : arr) {
std::cout << elem << ' ';
}
}Multi-dimensional arrays
// 4 rows Γ 3 columns
std::array<std::array<int, 3>, 4> grid{};
grid[1][2] = 42;
// Nested iteration
for (const auto& row : grid) {
for (int val : row) {
std::cout << val << ' ';
}
std::cout << '\n';
}Aggregate initialization
std::array is an aggregate: it has no user-declared constructors, so it is initialized with a brace-enclosed list of values. Providing fewer values than N zero-initializes the remaining elements. Providing more is a compile error.
// Partial initialization β trailing elements are zero-initialized
std::array<int, 5> a = {1, 2, 3}; // {1, 2, 3, 0, 0}
// Full zero-initialization (value-init syntax)
std::array<int, 5> b{}; // {0, 0, 0, 0, 0}
// Uninitialized β garbage for primitive types
std::array<int, 5> c; // indeterminate values β UB to read
// Over-initialized β compile error
// std::array<int, 3> d = {1, 2, 3, 4, 5}; // error: too many initializersContrast with raw int buf[5]. A raw array at block scope is also uninitialized unless you write int buf[5] = {}. The behavior is the same, but std::array makes the intent visible through the type system. int buf[5]{} and std::array<int, 5> b{} both zero-initialize.
CTAD and std::to_array
C++17: Class Template Argument Deduction
Since C++17 std::array ships with a deduction guide that eliminates the repetition of spelling out both T and N:
// C++17
std::array arr{1, 2, 3}; // deduces std::array<int, 3>
std::array floats{1.0f, 2.0f}; // deduces std::array<float, 2>
// Still required before C++17
std::array<int, 3> arr{1, 2, 3}; // C++11/14 β explicit T and NThe deduction guide only fires when all elements have the same type. Mixing types is a hard error.
// std::array mixed{1, 2.0}; // error: inconsistent element typesC++20: std::to_array
std::to_array (in <array>) is a factory function that constructs a std::array from a braced list or a raw C array, fully deducing both T and N. It is particularly useful for const/constexpr tables because it avoids the double specification of the element type and count:
#include <array> // C++20
// From a braced list β deduces array<int, 3>
auto a = std::to_array({1, 2, 3});
// Explicit type conversion β deduces array<long, 3>
auto b = std::to_array<long>({1, 2, 3});
// From a string literal β includes the null terminator
auto s = std::to_array("hello"); // array<char, 6> β {'h','e','l','l','o','\0'}
// From an existing C array β useful when wrapping legacy code
int raw[4] = {10, 20, 30, 40};
auto c = std::to_array(raw); // array<int, 4>
// constexpr factory β fully evaluated at compile time
constexpr auto primes = std::to_array({2, 3, 5, 7, 11, 13});std::to_array("hello") deducing array<char, 6> is the canonical way to produce a constexpr char array from a string literal without manually counting characters.
constexpr usage
std::array is fully constexpr-aware since C++17 (C++11 had partial support). This makes it the ideal type for compile-time lookup tables, replacing both raw arrays and manual template metaprogramming.
#include <array>
#include <cmath> // C++20
// Compile-time lookup table β evaluated entirely at compile time
constexpr std::array<double, 12> monthly_rates = {
0.031, 0.028, 0.031, 0.030,
0.031, 0.030, 0.031, 0.031,
0.030, 0.031, 0.030, 0.031
};
// Accessed as a constant expression
constexpr double jan_rate = monthly_rates[0]; // 0.031 β no runtime cost
// static_assert on array values β verified at compile time
static_assert(monthly_rates.size() == 12, "Must have one entry per month");
static_assert(monthly_rates[0] > 0.0, "January rate must be positive");
// constexpr function returning std::array
constexpr std::array<int, 5> make_squares() {
std::array<int, 5> result{};
for (int i = 0; i < 5; ++i) {
result[i] = (i + 1) * (i + 1);
}
return result;
}
constexpr auto squares = make_squares(); // {1, 4, 9, 16, 25}
static_assert(squares[2] == 9);Contrast with constexpr T[N]. Raw arrays can also be constexpr, but they decay to a pointer the moment you pass them to a function, and calling .size() on them is not possible:
// Raw constexpr array β loses size information at boundaries
constexpr int raw_rates[12] = { /* ... */ };
constexpr int len = sizeof(raw_rates) / sizeof(raw_rates[0]); // awkward
// std::array β size always accessible
constexpr auto rates = std::to_array<double>({0.031, 0.028 /*, ... */});
constexpr std::size_t len2 = rates.size(); // clean, no division trickStructured bindings (C++17)
std::array satisfies the std::tuple_size and std::tuple_element protocol, so it works with structured bindings just like std::tuple or std::pair.
// C++17
std::array<int, 3> point = {10, 20, 30};
// Bind by value β copies each element
auto [x, y, z] = point;
// Bind by const reference β no copy, elements read-only
const auto& [cx, cy, cz] = point;
// Bind by mutable reference β modifies the original array
auto& [rx, ry, rz] = point;
rx = 99;
// point[0] is now 99
// Works with CTAD (C++17)
auto [a, b, c] = std::array{1, 2, 3};The number of bindings must exactly match N. A mismatch is a hard compile error β the check is purely static:
std::array<int, 3> arr{1, 2, 3};
auto [p, q] = arr; // error: too few bindings for array of size 3
auto [p, q, r, s] = arr; // error: too many bindingsStructured bindings are especially useful for returning small fixed-size results from functions:
// Return an RGB triple as a plain array
constexpr std::array<uint8_t, 3> hex_to_rgb(uint32_t hex) {
return {
static_cast<uint8_t>((hex >> 16) & 0xFF),
static_cast<uint8_t>((hex >> 8) & 0xFF),
static_cast<uint8_t>((hex >> 0) & 0xFF)
};
}
auto [r, g, b] = hex_to_rgb(0xFF8800); // r=255, g=136, b=0Multi-dimensional arrays
std::array<std::array<int, Cols>, Rows> is a stack-allocated 2D matrix. The outer array has Rows elements, each of which is an inner array of Cols integers. The memory layout is identical to int mat[Rows][Cols].
// 2 rows Γ 3 columns
std::array<std::array<int, 3>, 2> mat = {{
{1, 2, 3},
{4, 5, 6}
}};
// Access: [row][col]
int val = mat[1][2]; // 6
// Iteration: outer = rows, inner = columns
for (const auto& row : mat) {
for (int v : row) {
std::cout << v << ' ';
}
std::cout << '\n';
}
// Dimensions are compile-time constants
constexpr std::size_t rows = mat.size(); // 2
constexpr std::size_t cols = mat[0].size(); // 3Structured bindings on nested arrays. Each row can be destructured independently:
// C++17
for (const auto& row : mat) {
auto [a, b, c] = row; // binds each column of the current row
std::cout << a << ' ' << b << ' ' << c << '\n';
}Compare with int arr[2][3]. The raw version and std::array version have identical memory layouts, but the raw version decays to int (*)[3] when passed to a function, losing the outer dimension:
// Raw array β outer dimension lost at the call site
void process_raw(int mat[][3], int rows) { /* rows passed manually */ }
// std::array β both dimensions preserved in the type
void process(const std::array<std::array<int, 3>, 2>& mat) {
constexpr auto r = mat.size(); // 2, from the type
constexpr auto c = mat[0].size(); // 3, from the type
}Comparison and sorting
std::array supports all six comparison operators using lexicographic ordering: two arrays are compared element-by-element from left to right, stopping at the first difference.
#include <array>
#include <algorithm>
std::array<int, 3> a = {1, 2, 3};
std::array<int, 3> b = {1, 2, 4};
std::array<int, 3> c = {1, 2, 3};
// Equality and inequality (C++11)
bool eq = (a == c); // true β all elements match
bool neq = (a != b); // true β differ at index 2
// Relational operators β lexicographic (C++11)
bool lt = (a < b); // true β first differing position: 3 < 4
bool gt = (b > a); // true
bool lte = (a <= c); // true β equal counts as <=
bool gte = (b >= a); // true
// Three-way comparison (C++20)
auto cmp = (a <=> b); // std::strong_ordering::lessSorting. Because std::array exposes begin() and end(), it integrates directly with any STL algorithm:
std::array<int, 5> arr = {5, 3, 1, 4, 2};
// Sort in ascending order (C++11)
std::sort(arr.begin(), arr.end()); // {1, 2, 3, 4, 5}
// Sort in descending order (C++11)
std::sort(arr.begin(), arr.end(), std::greater<int>{});
// Ranges sort β no explicit begin/end needed (C++20)
std::ranges::sort(arr);
// Custom comparator with ranges (C++20)
struct Point { int x, y; };
std::array<Point, 3> pts = {{{3,1},{1,4},{2,2}}};
std::ranges::sort(pts, {}, &Point::x); // sort by x fieldstd::get<I> and std::tuple_size
std::array participates in the tuple protocol, meaning you can use the same generic machinery that works with std::tuple and std::pair.
#include <array>
#include <tuple>
std::array<int, 3> arr = {10, 20, 30};
// std::get<I> returns a reference (C++11)
int& first = std::get<0>(arr); // 10 β modifiable lvalue ref
first = 99;
// arr is now {99, 20, 30}
// Compile-time size query (C++11)
constexpr std::size_t n = std::tuple_size_v<decltype(arr)>; // 3
// std::tuple_element gives the element type (C++11)
using Elem = std::tuple_element_t<0, decltype(arr)>; // intGeneric function compatible with both std::tuple and std::array. Because both types model the same tuple-protocol, a single template can operate on either:
// Print all elements of any tuple-like type
template<typename Tuple, std::size_t... Is>
void print_impl(const Tuple& t, std::index_sequence<Is...>) {
((std::cout << std::get<Is>(t) << ' '), ...); // C++17 fold
std::cout << '\n';
}
template<typename Tuple>
void print_tuple_like(const Tuple& t) {
print_impl(t, std::make_index_sequence<std::tuple_size_v<Tuple>>{});
}
// Works for both
std::array<int, 3> arr = {1, 2, 3};
std::tuple<int, double, std::string> tup = {1, 3.14, "hello"};
print_tuple_like(arr); // 1 2 3
print_tuple_like(tup); // 1 3.14 helloWhen to use std::array vs raw array vs std::vector
The decision hinges on three axes: where the data lives (stack vs heap), whether the size is known at compile time, and whether you need value semantics or a reference/pointer.
std::array<T,N> | T[N] (raw) | std::vector<T> | |
|---|---|---|---|
| Storage | Stack | Stack | Heap |
| Size | Compile-time constant | Compile-time constant | Runtime dynamic |
| Ownership / value semantics | Yes β copyable, movable | No β decays to pointer | Yes β copyable, movable |
| Size preserved across calls | Yes β part of the type | No β sizeof trick required | Yes β .size() |
constexpr | Full support (C++17) | Yes | No (heap) |
| Default overhead | None | None | Heap alloc + metadata |
| STL algorithm support | Full | Via pointer/length | Full |
The decay trap: raw arrays at function boundaries
The single biggest reason to prefer std::array over raw T[N] is that raw arrays decay to a pointer when passed to a function, silently losing size information:
// Raw array β outer dimension decays to pointer
void fill_raw(int arr[], int n) { // int arr[] == int* arr
for (int i = 0; i < n; ++i) { // size must be passed manually
arr[i] = i;
}
}
// std::array β size encoded in the type, never decays
template<std::size_t N>
void fill_array(std::array<int, N>& arr) {
for (std::size_t i = 0; i < arr.size(); ++i) { // size always known
arr[i] = static_cast<int>(i);
}
}
int raw[5];
std::array<int, 5> arr{};
fill_raw(raw, 5); // manual size β error-prone
fill_array(arr); // size comes from the type β safeDecision guide
Need a fixed number of elements known at compile time?
YES β Is the count large (> tens of thousands)?
YES β std::vector (avoids stack overflow)
NO β std::array (zero overhead, value semantics)
NO β std::vector
Is this a lookup table or constant-expression context?
YES β constexpr std::array (or std::to_array in C++20)
Interfacing with a C API that takes T*?
β std::array is fine β pass arr.data() + arr.size()
Passing to a function that accepts multiple container types?
β std::span<T> (C++20) β works with array, vector, and raw pointersBest Practices
Use at() for untrusted indices. operator[] offers no bounds checking; prefer at() when the index comes from user input or external data.
Reach for std::span in generic code. If your function doesn't require the exact compile-time size, accept std::span<T> (C++20) instead of std::array<T, N> to work with vectors, C arrays, and other contiguous sequences.
Stack-allocate small, fixed-size data. std::array is ideal for compile-time-sized buffers: parsing stacks, local work buffers, cache-friendly small tables. For dynamic sizes, use std::vector.
Use structured bindings to unpack results. When decomposing small fixed-size tuples or arrays, structured bindings (C++17) are clearer than explicit indexing:
auto [width, height] = image.dimensions(); // more readablePrefer std::to_array for constexpr tables. In C++20, std::to_array({...}) is the cleanest way to create a constexpr lookup table β it deduces both T and N without any repetition.
Leverage constexpr computations. Sizes, comparisons, and data() are all constexpr-eligible; use this for compile-time metaprogramming and constant expressions.
Common Pitfalls
Stack overflow from over-sized arrays
// WRONG: allocates 4 MB on the stack
std::array<int, 1'000'000> huge;
// CORRECT: heap allocation
auto huge = std::make_unique<std::array<int, 1'000'000>>();
std::vector<int> huge(1'000'000);Typical stack size is 1β8 MB; large fixed arrays easily cause stack overflow.
Aggregate initialization truncation
std::array<int, 5> arr = {1, 2, 3}; // OK: remaining elements are 0
std::array<int, 3> arr = {1, 2, 3, 4, 5}; // COMPILER ERRORUnlike C arrays, std::array enforces exact-size or partial (zero-padded) initialization.
Default-initialization leaves primitives uninitialized
std::array<int, 10> raw; // undefined values β garbage
std::array<int, 10> clean{}; // all zeros β deterministicAlways use {} if determinism matters. Default-construction of primitives is uninitialized.
Move is not O(1) for std::array
std::array<int, 1000> a = { /* ... */ };
auto b = std::move(a); // still O(N) β copies all 1000 intsUnlike std::vector, moving a std::array is a full element-wise copy because the size is part of the type. For true zero-cost moves with dynamic-sized containers, use std::vector or move pointers/references instead.
Mixing types in CTAD deduction
std::array arr1{1, 2, 3}; // OK: all int
// std::array arr2{1, 2.0, 3}; // error: int and double β cannot deduce T
std::array<double, 3> arr3{1, 2.0, 3}; // OK: explicit T resolves ambiguityAll initializers must have identical type for CTAD to fire. Provide an explicit template argument when mixing numeric types.
std::to_array copies, not moves, its argument
// The source array is copied into the new std::array object.
// For large arrays of non-trivially-copyable types, this may be expensive.
auto expensive = std::to_array(source); // O(N) copystd::to_array always produces a new object by copying (or moving from rvalue elements). It is not a zero-cost wrapper.
See Also
std::vectorβ Dynamic-sized contiguous container; use when size is not known at compile time.std::span(C++20) β Non-owning view of contiguous sequences; prefer for generic functions.std::get<I>()β Tuple-like access by compile-time index.std::tuple_sizeβ Type trait for obtaining the size of astd::array.std::to_array(C++20) β Factory function that deduces bothTandNfrom an initializer list or raw array.std::dequeβ Double-ended queue with efficient front insertion/removal.