C++ / STANDARD CONTAINERS
std::array for fixed-size sequences
Store a fixed number of elements with no allocation using std::array, and handle its length-in-the-type initialization, copying, and function signatures.
What you will learn
- Declare std::array<T, N> with a named constexpr N and read the length back from size()
- Zero every element with {}; a bare declaration leaves int elements indeterminate
- Accept any length by templating a function on std::size_t N instead of hardcoding it
- Pass by const reference: copying a std::array copies all N elements, it never decays
Understanding std::array for fixed-size sequences
std::array<T, N> is a class template whose entire state is one C array member, roughly T elems[N]. Nothing is allocated: the elements sit inside the object, so a local std::array lives on the stack, a static one in static storage, and a member one inside its owner. There is no size field either, because N is a template argument the compiler already knows, which is why sizeof(std::array<int, 5>) is exactly 5 * sizeof(int) and why size() is a constant expression. The mental model is a raw array that finally has the member functions, iterators, and copy semantics you expect from a container.
Because N lives in the type, std::array<int, 3> and std::array<int, 4> are as unrelated as int and double. That rules out push_back and resize, and it means a function parameter pins the length unless you template on it, as in template <std::size_t N> void f(const std::array<int, N>&), or take a std::span. In exchange the length is usable in constant expressions: static_assert(a.size() == 3), std::get<2>(a) with a compile-time-checked index, structured bindings, and tables computed entirely at compile time.
std::array also fixes what raw arrays get wrong: it assigns, copies, compares, and can be returned from a function by value, and it never decays to a pointer, so the length survives every call. The flip side is that a by-value parameter copies all N elements, and swap() is an O(N) element-wise exchange rather than the pointer swap a heap-backed container performs, because there is no buffer to hand over. Initialization follows aggregate rules: braces list the initial values, missing ones are value-initialized, and a declaration with no braces at all leaves trivially-typed elements indeterminate.
Reach for std::array whenever the count is known while you are writing the code, and for a vector when it is not.
<array>
<iostream>
<numeric>
<stdexcept>
// N is a template parameter, so one function handles every length
// and no separate count argument is needed.
template <std::size_t N>
int sum(const std::array<int, N>& a) {
return std::accumulate(a.begin(), a.end(), 0);
}
int main() {
std::array<int, 5> primes{2, 3, 5, 7, 11};
std::cout << std::boolalpha;
std::cout << "size: " << primes.size() << '\n';
std::cout << "elements only: " << (sizeof(primes) == 5 * sizeof(int)) << '\n';
std::cout << "ends: " << primes.front() << ' ' << primes.back() << '\n';
std::cout << "sum: " << sum(primes) << '\n';
std::array<int, 5> other = primes; // copies all five ints
other[0] = 99;
std::cout << "original untouched: " << primes[0] << '\n';
try {
std::cout << primes.at(5) << '\n'; // [] here would be silent UB
} catch (const std::out_of_range&) {
std::cout << "at(5) threw out_of_range\n";
}
}
The length of a std::array is part of its type, so the elements live directly inside the object with no allocation and no runtime size field.
Worked examples
A table built at compile time
Shows that the length and even the contents of a std::array can be constant expressions.
<array>
<iostream>
// C++17: a constexpr function can fill and return a std::array,
// so this table exists before the program starts running.
constexpr std::array<int, 6> squares() {
std::array<int, 6> t{}; // {} zeroes; without it the ints are indeterminate
for (std::size_t i = 0; i < t.size(); ++i)
t[i] = static_cast<int>(i * i);
return t;
}
int main() {
constexpr auto table = squares();
static_assert(table.size() == 6);
static_assert(table[4] == 16);
for (std::size_t i = 0; i < table.size(); ++i)
std::cout << (i == 0 ? "" : " ") << table[i];
std::cout << '\n' << std::get<3>(table) << '\n';
}
Example explained
Line 1std::array<int, 6> t{}; value-initializes all six ints to 0; the braces are what make that happen.
Line 2Returning the array by value from squares() is safe because the elements are part of the object, so there is nothing to dangle.
Line 3static_assert(table[4] == 16) is checked by the compiler, which only works because both the length and the values are compile-time constants.
Line 4std::get<3> takes the index as a template argument, so an out-of-range index is a compile error instead of undefined behaviour.
Algorithms, C APIs, and swap
Demonstrates deduction of the length, why data() is needed for pointer-based interfaces, and that swap moves values rather than buffers.
<algorithm>
<array>
<iostream>
// A C-style interface: pointer plus length.
long total(const int* p, std::size_t n) {
long s = 0;
for (std::size_t i = 0; i < n; ++i) s += p[i];
return s;
}
int main() {
std::array a{5, 1, 4}; // C++17 deduces std::array<int, 3>
std::sort(a.begin(), a.end());
std::cout << a[0] << a[1] << a[2] << '\n';
std::cout << total(a.data(), a.size()) << '\n';
std::array<int, 3> b{9, 9, 9};
a.swap(b); // element-wise, O(N)
std::cout << a[0] << ' ' << b[0] << '\n';
auto [x, y, z] = b;
std::cout << x + y + z << '\n';
}
Example explained
Line 1std::array a{5, 1, 4}; uses class template argument deduction: the element type comes from the initializers, the length from how many there are.
Line 2total(a.data(), a.size()) is the only way in, because a std::array does not implicitly convert to int* the way a raw array does.
Line 3a.swap(b) exchanges three int values one pair at a time, so &a[0] still points into a; a heap-backed container could instead just trade pointers.
Line 4auto [x, y, z] = b; works because the element count is fixed in the type, and it copies b rather than referring to it.
A grid without row pointers
Shows how nesting std::array gives a 2D layout that is a single contiguous block.
<array>
<iostream>
int main() {
// 2 rows of 3: six ints in one block, no pointers between rows.
std::array<std::array<int, 3>, 2> grid{{{1, 2, 3}, {4, 5, 6}}};
std::cout << grid[1][2] << '\n';
std::cout << (sizeof(grid) == 6 * sizeof(int) ? "packed" : "padded") << '\n';
for (const auto& row : grid) {
for (int v : row) std::cout << v;
std::cout << '\n';
}
}
Example explained
Line 1The outer braces initialize the std::array aggregate, the next pair its internal C array, then one pair per row; dropping a level relies on brace elision and often triggers a warning.
Line 2grid[1][2] chains two operator[] calls with no pointer dereference in between, so the second index is only an offset within the row.
Line 3sizeof(grid) is exactly six ints because neither array level adds bookkeeping, which is what makes the whole grid usable as one buffer.
Line 4for (const auto& row : grid) binds row to a std::array<int, 3>, which is itself iterable, so the inner range-for needs nothing extra.
Important notes
Fewer initializers than N is legal and the rest are value-initialized, so std::array<int, 5> a{1, 2}; silently gives {1, 2, 0, 0, 0} rather than an error.
The elements live inside the object, so a local std::array<int, 1'000'000> puts 4 MB on the stack and can overflow it; that size belongs in a heap-allocated container.
Common mistakes
Writing std::array<int, 4> a; and then reading a[0]: unlike std::vector<int>(4), nothing is zeroed, so you read an indeterminate value and the bug shifts when you change compiler or flags. Write std::array<int, 4> a{}.
Treating the length as adjustable: there is no push_back or resize, and a parameter of type const std::array<int, 3>& will not accept a 4-element array, so the call fails to compile until you template on N or use std::span.
Passing a large std::array by value out of habit from raw arrays that decayed to pointers: each call copies all N elements and any write inside the function is lost, since the callee mutated its own copy.
Try it yourself
Change, predict, then run
Build a constexpr std::array<int, 10> holding the first ten triangular numbers, n*(n+1)/2, then write a function template taking const std::array<int, N>& that returns how many elements are even, and print the result for that array and for std::array{2, 4, 5}.
Open the C++ workspaceCheck your understanding
You have std::array<double, 512> buf; in a function and call void process(std::array<double, 512> a). What happens at that call?
- buf decays to a double*, so only an address is passed
- All 512 doubles are copied into the parameter, and changes process makes are invisible to the caller
- Only a pointer and a length are copied, so the call is as cheap as passing a view
- It fails to compile, because std::array parameters must be references
Show answer
std::array is an ordinary class with value semantics, so a by-value parameter copies every element, 4096 bytes here, and process writes into its own copy. The first option is tempting because a raw double buf[512] parameter really is adjusted to double*, but std::array has no such implicit conversion; you only get a pointer by calling data(), and to let the callee modify the original you must take a reference.