Skip to content
C++
Language
Basic

Arrays

Built-in arrays, std::array, decay, multidimensional layout, initialization rules, and modern replacements, with C++ standard versions noted.

Arraysince C++98

An 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] and int[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::vector for runtime sizes.
  • They are not assignable or copyable by value. a = b is 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 to T* 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

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

cpp
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++20

Examples

Passing arrays without losing the size

A parameter declared int p[10] is silently adjusted to int* p. Three alternatives preserve the extent:

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

cpp
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++11

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

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

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

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

cpp
#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::array for fixed sizes. It has zero overhead over T[N]. It also provides size(), 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) or std::ranges::size (C++20) instead of the sizeof division idiom. The std::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 to std::array without specifying the type and size twice.
  • Zero-initialize explicitly (int a[N] {}) for automatic arrays you will read before writing.
  • Use constexpr for lookup tables. A static constexpr std::array gives compile-time evaluation and avoids runtime initialization order problems.
  • Use std::vector, not new[], for runtime-sized storage. If you must use new[], hold it in std::unique_ptr<T[]> (C++11) or std::make_unique<T[]>(n) (C++14, value-initializes).
  • Apply alignas on the array declaration for SIMD or cache-line alignment: alignas(64) float lanes[16];.

Common Pitfalls

1. Array-to-pointer decay breaks sizeof.

cpp
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