Skip to content
C++
Library
since C++11
Basic

std::array

Fixed-size contiguous array wrapper with value semantics, bounds checking, and full STL compatibility.

std::arraysince C++11

A 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() throws std::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

cpp
#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 access

Examples

Construction and initialization

cpp
#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

cpp
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

cpp
#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);     // false

Generic functions and std::span

cpp
// 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

cpp
// 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.

cpp
// 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 initializers

Contrast 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:

cpp
// 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 N

The deduction guide only fires when all elements have the same type. Mixing types is a hard error.

cpp
// std::array mixed{1, 2.0};   // error: inconsistent element types

C++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:

cpp
#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.

cpp
#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:

cpp
// 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 trick

Structured 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.

cpp
// 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:

cpp
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 bindings

Structured bindings are especially useful for returning small fixed-size results from functions:

cpp
// 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=0

Multi-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].

cpp
// 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();      // 3

Structured bindings on nested arrays. Each row can be destructured independently:

cpp
// 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:

cpp
// 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.

cpp
#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::less

Sorting. Because std::array exposes begin() and end(), it integrates directly with any STL algorithm:

cpp
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 field

std::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.

cpp
#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)>;   // int

Generic function compatible with both std::tuple and std::array. Because both types model the same tuple-protocol, a single template can operate on either:

cpp
// 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 hello

When 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>
StorageStackStackHeap
SizeCompile-time constantCompile-time constantRuntime dynamic
Ownership / value semanticsYes β€” copyable, movableNo β€” decays to pointerYes β€” copyable, movable
Size preserved across callsYes β€” part of the typeNo β€” sizeof trick requiredYes β€” .size()
constexprFull support (C++17)YesNo (heap)
Default overheadNoneNoneHeap alloc + metadata
STL algorithm supportFullVia pointer/lengthFull

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:

cpp
// 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 β€” safe

Decision guide

cpp
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 pointers

Best 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:

cpp
auto [width, height] = image.dimensions();  // more readable

Prefer 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

cpp
// 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

cpp
std::array<int, 5> arr = {1, 2, 3};        // OK: remaining elements are 0
std::array<int, 3> arr = {1, 2, 3, 4, 5}; // COMPILER ERROR

Unlike C arrays, std::array enforces exact-size or partial (zero-padded) initialization.

Default-initialization leaves primitives uninitialized

cpp
std::array<int, 10> raw;     // undefined values β€” garbage
std::array<int, 10> clean{}; // all zeros β€” deterministic

Always use {} if determinism matters. Default-construction of primitives is uninitialized.

Move is not O(1) for std::array

cpp
std::array<int, 1000> a = { /* ... */ };
auto b = std::move(a);  // still O(N) β€” copies all 1000 ints

Unlike 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

cpp
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 ambiguity

All 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

cpp
// 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) copy

std::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 a std::array.
  • std::to_array (C++20) β€” Factory function that deduces both T and N from an initializer list or raw array.
  • std::deque β€” Double-ended queue with efficient front insertion/removal.