Arrays
Built-in arrays, std::array, decay, multidimensional layout, initialization rules, and modern replacements, with C++ standard versions noted.
Arraysince C++98An array is a contiguous sequence of N objects of the same type, where N is a compile-time constant that is part of the array's type.
Overview
C++ has two kinds of fixed-size arrays. The first is the built-in (C-style) array, T[N]. The second is std::array<T, N> (C++11), a thin aggregate wrapper that has the same layout and performance but behaves like a regular value type.
Key properties of built-in arrays:
- The size is part of the type.
int[3]andint[4]are different types. - The bound must be a constant expression. Variable-length arrays (VLAs) are not standard C++. GCC and Clang accept them as an extension, and MSVC rejects them. Use
std::vectorfor runtime sizes. - They are not assignable or copyable by value.
a = bis ill-formed for built-in arrays. Arrays also cannot be returned from functions, and they decay when passed as arguments. - Elements are contiguous and zero-indexed.
a[i]is defined as*(a + i). Indexing is unchecked, so out-of-range access is undefined behavior. - Decay. In most expression contexts,
T[N]converts implicitly toT*pointing at the first element. The size information is lost.
In modern code, prefer std::array (size known at compile time), std::vector (size known at run time), or std::span (C++20, a non-owning view). Built-in arrays remain common in legacy code, C interop, and string literals (const char[N]).
Syntax
#include <array>
#include <cstddef>
#include <iterator>
#include <span> // C++20
int a[5]; // uninitialized (automatic storage): indeterminate values
int b[5] = {}; // all zero (value-initialized)
int c[5] {1, 2}; // {1, 2, 0, 0, 0} (C++11 brace form)
int d[] = {1, 2, 3}; // bound deduced: int[3]
const char s[] = "hi"; // const char[3], includes '\0'
constexpr std::size_t N = 8;
double buf[N]; // OK: N is a constant expression
std::array<int, 3> e {1, 2, 3}; // C++11
std::array f {1, 2, 3}; // C++17: CTAD deduces std::array<int, 3>
auto g = std::to_array({1, 2, 3}); // C++20: deduce from a braced list or array
int* heap = new int[n]{}; // dynamic array of runtime size; prefer std::vector
delete[] heap; // must match new[]Size queries:
int arr[10];
sizeof(arr) / sizeof(arr[0]); // 10, works only on a true array, not a pointer
std::size(arr); // C++17: 10, ill-formed if arr is a pointer
std::ssize(arr); // C++20: signed result (std::ptrdiff_t)
std::extent_v<decltype(arr)>; // C++17 variable template (trait itself is C++11)
std::rank_v<int[3][4]>; // 2 (C++17 variable template; trait is C++11)
std::is_bounded_array_v<int[3]>; // C++20Examples
Passing arrays without losing the size
A parameter declared int p[10] is silently adjusted to int* p. Three alternatives preserve the extent:
#include <array>
#include <numeric>
#include <span>
// 1. Reference to array: size is enforced at compile time
template <std::size_t N>
int sum_ref(const int (&a)[N]) {
return std::accumulate(a, a + N, 0);
}
// 2. std::array by const reference
template <std::size_t N>
int sum_arr(const std::array<int, N>& a) {
return std::accumulate(a.begin(), a.end(), 0);
}
// 3. std::span (C++20): works for built-in arrays, std::array, and vector
int sum_span(std::span<const int> s) {
return std::accumulate(s.begin(), s.end(), 0);
}
int main() {
int raw[] = {1, 2, 3, 4};
std::array<int, 4> stl {1, 2, 3, 4};
return sum_ref(raw) + sum_arr(stl) + sum_span(raw) + sum_span(stl); // 40
}Iteration
int scores[] = {90, 72, 85};
for (int s : scores) { /* ... */ } // range-for, C++11, works on arrays of known bound
for (auto it = std::begin(scores); it != std::end(scores); ++it) { } // std::begin/end, C++11Range-based for works on an array only when its bound is known. A decayed pointer is rejected at compile time.
Multidimensional arrays
A built-in 2D array is an array of arrays, laid out in row-major order. Only the first bound may be omitted when initializing.
int grid[3][4] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12},
};
int inferred[][2] = {{1, 2}, {3, 4}, {5, 6}}; // first bound deduced: int[3][2]
// Rows are contiguous; grid[r][c] is at offset r*4 + c
static_assert(sizeof(grid) == 12 * sizeof(int));
// Passing: all bounds after the first are required
void process(int (*rows)[4], std::size_t n); // pointer to int[4]
void process2(int (&g)[3][4]); // exact referenceFor nested standard arrays, use std::array<std::array<int, 4>, 3>. This type stays contiguous and is copyable, but its initialization needs care (see Pitfalls). For flat storage with a 2D view, use std::mdspan (C++23) over a single buffer:
#include <mdspan> // C++23
#include <vector>
std::vector<int> data(3 * 4);
std::mdspan m(data.data(), 3, 4); // CTAD, C++23
m[1, 2] = 42; // multidimensional subscript (C++23)The multidimensional operator[] is C++23. Before that, the comma expression m[1, 2] was deprecated (C++20) because it silently meant m[2]. It does not apply to built-in arrays, which still require grid[1][2].
std::array as a constexpr lookup table
#include <array>
constexpr auto make_squares() { // constexpr function: C++14 for loops
std::array<int, 8> t {};
for (std::size_t i = 0; i < t.size(); ++i)
t[i] = static_cast<int>(i * i); // non-const constexpr operator[]: C++17
return t;
}
constexpr auto squares = make_squares();
static_assert(squares[5] == 25);Mutating a std::array in a constant expression requires C++17, because the non-const operator[] and begin() became constexpr in that standard. In C++14 only the const accessors were constexpr.
Best Practices
- Default to
std::arrayfor fixed sizes. It has zero overhead overT[N]. It also providessize(), iterators,at(),fill(), comparison operators, and copy/assignment, and it can be returned by value. - Use
std::span<T>(C++20) for function parameters that accept any contiguous sequence. Before C++20, pass a pointer plus length, or a template on the container. - Use
std::size()(C++17) orstd::ranges::size(C++20) instead of thesizeofdivision idiom. Thestd::size()form fails to compile on pointers rather than computing a wrong value. - Use
std::to_array(C++20) to convert a C array or braced list tostd::arraywithout specifying the type and size twice. - Zero-initialize explicitly (
int a[N] {}) for automatic arrays you will read before writing. - Use
constexprfor lookup tables. Astatic constexpr std::arraygives compile-time evaluation and avoids runtime initialization order problems. - Use
std::vector, notnew[], for runtime-sized storage. If you must usenew[], hold it instd::unique_ptr<T[]>(C++11) orstd::make_unique<T[]>(n)(C++14, value-initializes). - Apply
alignason the array declaration for SIMD or cache-line alignment:alignas(64) float lanes[16];.
Common Pitfalls
1. Array-to-pointer decay breaks sizeof.
void f(int a[10]) {
auto n = sizeof(a) / sizeof(a[0]); // sizeof(int*)/sizeof(int): usually 2, not 10
}Compilers warn (-Wsizeof-array-argument), but the code still compiles. std::size(a) correctly fails here.
2. Off-by-one and unchecked indexing. a[N] is one past the end. You may form its address but not dereference it. std::array::at() and std::vector::at() throw std::out_of_range. operator[] never checks. Use hardened standard library modes (_GLIBCXX_ASSERTIONS, libc++ hardening) to catch violations in testing.
3. Brace elision with nested std::array. std::array<std::array<int,2>,2> m {{1,2},{3,4}} can produce warnings or errors on some compilers. The most portable form is fully braced: m {{ {1,2}, {3,4} }}.
4. Polymorphic arrays. Base* p = new Derived[10]; p[1] indexes with sizeof(Base) stride, which is undefined behavior. Use std::vector<std::unique_ptr<Base>> for polymorphic collections.
5. Mismatched new[] / delete. Releasing with plain delete is undefined behavior. Use delete[], or better, avoid raw arrays.
6. Arrays are not first-class values. int a[3], b[3]; a = b; does not compile. Wrap in std::array or use std::copy. Likewise, auto x = arr; deduces a pointer (decay), while auto& x = arr; deduces a reference to the array.
7. String literal arrays. char s[] = "abc" copies the literal into a writable array. const char* s = "abc" points at read-only storage. Modifying the latter is undefined behavior, and since C++11 it is ill-formed to bind a string literal to non-const char*.
8. Comparing arrays with ==. For built-in arrays this compares addresses. C++20 deprecates the comparison, and C++26 makes it ill-formed. Use std::equal, std::ranges::equal (C++20), or std::array's operator==.
9. Non-trivial element lifetimes. new T[n] default-constructs every element, and delete[] destroys them in reverse order. For a partially initialized buffer, use std::vector::reserve or std::allocator rather than a raw array.
See Also
- Aggregate Initialization: rules for braced initialization of arrays and
std::array - Pointers: decay, pointer arithmetic, and one-past-the-end rules
- Range-based for: iteration over arrays of known bound
- alignas: over-aligning array storage