The Problem with Multidimensional std::array Initialization

In modern C++, creating and initializing a multidimensional std::vector with a specific default value is clean and straightforward:

constexpr int rows = 3;
constexpr int cols = 2;
std::vector<std::vector<double>> vec(rows, std::vector<double>(cols, -1.0));

However, std::array is an aggregate type with fixed, compile-time sizes and lacks fill constructors. If you want to initialize every element in a 2D or N-dimensional std::array to an arbitrary non-zero value (such as -1.0 or NAN), hardcoding initializer lists becomes impossible for large dimensions, and manually creating temporary rows feels verbose.

Let’s explore the best and most idiomatic ways to initialize multidimensional std::array instances in modern C++.

1. The Cleanest In-Place Solution: Range-Based Loop

If you don't need a single-line initialization or const declaration, a range-based for loop is the most readable and zero-overhead solution:

#include <array>
#include <cmath>

int main() {
    constexpr size_t rows = 3;
    constexpr size_t cols = 2;

    std::array<std::array<double, cols>, rows> arr;
    for (auto& row : arr) {
        row.fill(-1.0);
    }
}

Why it works well: It communicates intent clearly, compiles down to efficient vectorization or memset operations (where applicable), and avoids template bloat.

2. The Modern One-Liner: A constexpr Factory Function

If you require const/constexpr initialization or want a one-line declaration similar to std::vector, write a reusable factory function using index sequences (C++14/17/20):

#include <array>
#include <utility>
#include <cmath>

template <typename T, std::size_t Cols, std::size_t... Is>
constexpr auto make_row_impl(const T& val, std::index_sequence<Is...>) {
    return std::array<T, Cols>{ ((void)Is, val)... };
}

template <typename T, std::size_t Cols>
constexpr auto make_row(const T& val) {
    return make_row_impl<T, Cols>(val, std::make_index_sequence<Cols>{});
}

template <typename T, std::size_t Rows, std::size_t Cols, std::size_t... Is>
constexpr auto make_2d_array_impl(const T& val, std::index_sequence<Is...>) {
    return std::array<std::array<T, Cols>, Rows>{ ((void)Is, make_row<T, Cols>(val))... };
}

template <typename T, std::size_t Rows, std::size_t Cols>
constexpr auto make_2d_array(const T& val) {
    return make_2d_array_impl<T, Rows, Cols>(val, std::make_index_sequence<Rows>{});
}

int main() {
    // Clean 1-line initialization (usable at compile time!)
    constexpr auto arr_1 = make_2d_array<double, 3, 2>(-1.0);
    auto arr_2 = make_2d_array<double, 3, 2>(NAN);
}

3. Fully Generic N-Dimensional Array Generator (C++20)

For arbitrary dimensions (2D, 3D, N-D), you can generalize the factory recursively with C++20:

#include <array>
#include <utility>

template <typename T, std::size_t Dim, std::size_t... RestDims>
struct nd_array {
    using type = std::array<typename nd_array<T, RestDims...>::type, Dim>;
};

template <typename T, std::size_t Dim>
struct nd_array<T, Dim> {
    using type = std::array<T, Dim>;
};

template <typename T, std::size_t... Dims>
using nd_array_t = typename nd_array<T, Dims...>::type;

template <typename T, std::size_t Dim, std::size_t... Is>
constexpr auto make_filled_array_impl(const T& val, std::index_sequence<Is...>) {
    return std::array<T, Dim>{ ((void)Is, val)... };
}

template <typename T, std::size_t Dim, std::size_t NextDim, std::size_t... RestDims, std::size_t... Is>
constexpr auto make_filled_array_impl(const T& val, std::index_sequence<Is...>) {
    return std::array<nd_array_t<T, NextDim, RestDims...>, Dim>{
        ((void)Is, make_filled_array_impl<T, NextDim, RestDims...>(val, std::make_index_sequence<NextDim>{}))...
    };
}

template <typename T, std::size_t... Dims>
constexpr auto make_filled_array(const T& val) {
    return make_filled_array_impl<T, Dims...>(val, std::make_index_sequence<std::get<0>(std::tuple{Dims...})>{});
}

int main() {
    // Creates a 3x2x4 array filled with -1.0
    constexpr auto arr_3d = make_filled_array<double, 3, 2, 4>(-1.0);
}

4. Alternative: Flat Array with a 2D View (std::mdspan)

In C++23, an increasingly popular paradigm is to avoid nested arrays altogether. Nested arrays can introduce unnecessary type complexity. Instead, use a flat 1D array filled in a single call and access it through std::mdspan:

#include <array>
#include <mdspan>

int main() {
    constexpr size_t rows = 3;
    constexpr size_t cols = 2;

    // 1D storage: easy to fill
    std::array<double, rows * cols> storage;
    storage.fill(-1.0);

    // 2D access view
    auto matrix = std::mdspan(storage.data(), rows, cols);
    
    // matrix[i, j] provides clean 2D indexing
    double val = matrix[0, 1]; // -1.0
}

Summary

  • Quick and simple: Use a range-based for (auto& row : arr) row.fill(val);.
  • Single-line & constexpr: Implement a small make_filled_array<T, Rows, Cols>(value) helper function.
  • C++23 Recommended: Prefer flat 1D contiguous storage paired with std::mdspan for cleaner multidimensional abstractions and trivial .fill() support.