How to Initialize All Elements of a Multidimensional std::array to a Default Value in C++
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 smallmake_filled_array<T, Rows, Cols>(value)helper function. - C++23 Recommended: Prefer flat 1D contiguous storage paired with
std::mdspanfor cleaner multidimensional abstractions and trivial.fill()support.