Fixed Multi-Dimensional Arrays
Create nested std::array types for multi-dimensional fixed-size storage.
What Are Multidimensional std::array?
There is no multidimensional array class in the standard library. std::array is one-dimensional, and the way you get a second dimension is to make the element type another std::array.
That works, and it keeps the compile-time length information a C-style multidimensional array gives you, but the syntax fights back. This lesson covers the nesting, the alias template that makes it bearable, how to ask for each dimension's length safely, and the flattening approach that sidesteps the whole problem.
Nesting One Array Inside Another
A C-style 3 by 4 grid is written int board[3][4]. The std::array equivalent puts the inner array in the element position:
std::array<std::array<int, 4>, 3> board; // 3 rows of 4
Three things are worth noticing straight away, and only one of them is pleasant.
The dimensions read backwards. You want 3 rows of 4 columns, and the type says 4 first. That falls out of how the nesting works: the outer array holds 3 elements, each of which is an array of 4.
Initialization needs double braces. The outer array is an aggregate whose elements are themselves aggregates, so there is a brace level for the outer array and another for the inner ones.
It gets worse with each dimension. A three-dimensional version is another layer of the same.
Making It Readable With an Alias Template
A plain type alias would fix one specific shape:
using SmallIntBoard = std::array<std::array<int, 4>, 3>;
That helps exactly until you need a different size or element type, and then you need another alias. An alias template parameterises the alias instead, so the element type and both dimensions become template arguments:
#include <array>
#include <iostream>
template <typename T, std::size_t Height, std::size_t Width>
using Board = std::array<std::array<T, Width>, Height>;
template <typename T, std::size_t Height, std::size_t Width>
void showBoard(const Board<T, Height, Width>& board)
{
for (const auto& line : board)
{
for (const auto& tile : line)
std::cout << tile << ' ';
std::cout << '\n';
}
}
int main()
{
Board<int, 3, 4> board{ {
{ 7, 14, 21, 28 },
{ 35, 42, 49, 56 },
{ 63, 70, 77, 84 } } };
showBoard(board);
std::cout << "Tile at row 2, column 1: " << board[2][1] << '\n';
return 0;
}
Output:
7 14 21 28
35 42 49 56
63 70 77 84
Tile at row 2, column 1: 70
Board<int, 3, 4> now reads in the order you think in, because the alias puts Height before Width and hands them to the nested template in whatever order that template wants. Indexing is unchanged: board[2][1] is row 2, column 1, exactly as with a C-style array.
Note that a function taking a Board still has to declare the same three template parameters. The alias simplifies the spelling, not the genericity.
The same trick scales:
template <typename T, std::size_t Height, std::size_t Width, std::size_t Layers>
using Cube = std::array<std::array<std::array<T, Layers>, Width>, Height>;
Asking for the Dimensions
size() on a nested array reports the outer dimension only. The obvious way to reach the inner one is to index an element and ask that:
board[0].size(); // width, but only if there is a row 0
That is a trap. If the outer dimension is ever zero, board[0] indexes an element that does not exist and the program has undefined behavior before size() is ever called.
The safe version never touches the data at all. Because the lengths are template parameters, a function template can read them straight from the type:
#include <array>
#include <iostream>
template <typename T, std::size_t Height, std::size_t Width>
using Board = std::array<std::array<T, Width>, Height>;
template <typename T, std::size_t Height, std::size_t Width>
constexpr int heightOf(const Board<T, Height, Width>&)
{
return Height;
}
template <typename T, std::size_t Height, std::size_t Width>
constexpr int widthOf(const Board<T, Height, Width>&)
{
return Width;
}
int main()
{
Board<int, 3, 4> board{ {
{ 7, 14, 21, 28 },
{ 35, 42, 49, 56 },
{ 63, 70, 77, 84 } } };
std::cout << "Height: " << heightOf(board) << '\n';
std::cout << "Width: " << widthOf(board) << '\n';
return 0;
}
Output:
Height: 3
Width: 4
The parameter has no name because the value is never read; only its type matters. Returning int rather than std::size_t needs no cast, since converting a constexpr std::size_t to int is non-narrowing.
Flattening
Multidimensional arrays are verbose to declare, awkward to measure, and need one more nested loop per dimension to traverse. Flattening avoids all three by storing the same elements in a single dimension: a 3 by 4 board becomes a 12-element array.
The storage is then simple, but the two-dimensional access you actually wanted is gone. You get it back by wrapping the flat array in a view that maps a row and column onto one index, row * width + column:
#include <array>
#include <functional>
#include <iostream>
template <typename T, std::size_t Height, std::size_t Width>
using FlatBoard = std::array<T, Height * Width>;
template <typename T, std::size_t Height, std::size_t Width>
class BoardView
{
private:
std::reference_wrapper<FlatBoard<T, Height, Width>> m_tiles;
public:
BoardView(FlatBoard<T, Height, Width>& tiles)
: m_tiles{ tiles }
{
}
T& operator[](int index) { return m_tiles.get()[static_cast<std::size_t>(index)]; }
const T& operator[](int index) const { return m_tiles.get()[static_cast<std::size_t>(index)]; }
T& operator()(int row, int column) { return m_tiles.get()[static_cast<std::size_t>(row * width() + column)]; }
const T& operator()(int row, int column) const { return m_tiles.get()[static_cast<std::size_t>(row * width() + column)]; }
int height() const { return static_cast<int>(Height); }
int width() const { return static_cast<int>(Width); }
int tileCount() const { return static_cast<int>(Height * Width); }
};
int main()
{
FlatBoard<int, 3, 4> tiles{
7, 14, 21, 28,
35, 42, 49, 56,
63, 70, 77, 84 };
BoardView<int, 3, 4> board{ tiles };
std::cout << "Height: " << board.height() << " Width: " << board.width() << '\n';
for (int index{ 0 }; index < board.tileCount(); ++index)
std::cout << board[index] << ' ';
std::cout << '\n';
for (int row{ 0 }; row < board.height(); ++row)
{
for (int column{ 0 }; column < board.width(); ++column)
std::cout << board(row, column) << ' ';
std::cout << '\n';
}
board(1, 2) = 500;
std::cout << "After writing through the view: " << tiles[6] << '\n';
return 0;
}
Output:
Height: 3 Width: 4
7 14 21 28 35 42 49 56 63 70 77 84
7 14 21 28
35 42 49 56
63 70 77 84
After writing through the view: 500
Two design choices in there are worth explaining.
Why std::reference_wrapper rather than a reference member. A reference member cannot be reseated, which makes the whole class non-copy-assignable. std::reference_wrapper gives the same referring behavior while leaving the view assignable.
Why operator() for the two-dimensional access. Before C++23, operator[] accepted exactly one subscript, so the view uses [] for flat indexing and () for row-and-column. The alternative was to have operator[] return a sub-view that overloads operator[] again, which is more machinery and scales badly past two dimensions. From C++23 operator[] takes multiple subscripts and can serve both.
The last two lines make the point that this is a view, not a copy: writing through board(1, 2) changes tiles[6], since 1 * 4 + 2 is 6. The flat array must outlive the view.
std::mdspan
C++23 supplies this as a library type. std::mdspan is a modifiable view presenting a multidimensional interface over a contiguous sequence, so it replaces the hand-written view above. Modifiable means it is not read-only in the way std::string_view is: when the underlying elements are non-const, you can write through the span.
You construct it from a pointer to the data, which data() provides for std::array and std::vector, plus the extents. It calls the dimensions extents, reachable via extents().extent(0) and so on, and because C++23 allows multiple subscripts, indexing is span[row, column] rather than span[row][column]. Reaching the flat sequence again means going through data_handle().
C++26 adds std::mdarray, which is the owning version: std::array and std::mdspan combined into one type that holds its elements.
The code runner on this site compiles with
-std=c++20, so std::mdspan is not available in the editor here. The hand-written BoardView above does the same job and does compile.
Summary
No standard class: the library has no dedicated multidimensional array. std::array is one-dimensional, so a second dimension means nesting one inside another.
Nesting reverses the dimensions: std::array<std::array<int, 4>, 3> is 3 rows of 4, with the inner length written first, and initialization needs double braces because the outer aggregate holds inner aggregates.
Alias templates fix the readability: template <typename T, std::size_t Height, std::size_t Width> using Board = std::array<std::array<T, Width>, Height>; lets you write Board<int, 3, 4> in the order you think in.
Getting lengths safely: size() gives the outer dimension only, and reaching the inner one through board[0] is undefined behavior when the outer length is zero. Read the dimensions from the template parameters with a function template instead.
Flattening: store Height * Width elements in one dimension and wrap them in a view that maps row * width + column to a single index. Use std::reference_wrapper so the view stays assignable, and operator() for the two-dimensional access before C++23.
std::mdspan (C++23): a modifiable multidimensional view over a contiguous sequence, built from a data pointer and extents, indexed as span[row, column]. C++26 adds the owning std::mdarray.
How did you find this lesson?
Your rating helps us improve the content.
Create an account to track your progress and access interactive exercises. Already have one? Sign in.
Fixed Multi-Dimensional Arrays - Quiz
Test your understanding of the lesson.
Practice Exercises
Tic-Tac-Toe Board with Alias Template
Create a program that represents a tic-tac-toe game board using a 2D std::array with an alias template. Practice creating and using the Grid2d alias template for cleaner multidimensional array syntax.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!