Using Mdspan Class Template to Handle Multi-Dimensional Arrays in C++
This article explains the guidelines that will help you work with multi-dimensional arrays with the mdspan class template.
Join the DZone community and get the full member experience.
Join For FreeAlthough there are 4 million C++ programmers around the world, many of them lack the mastery needed to offer the services employers need. Therefore, there is still a shortage of these valuable experts.
The reason C++ programmers have such excellent job security is that the language is complex and difficult to master. As a result, demand for programmers is surging these days. C++ programmers, in particular, can find stable employment. There are many great C++ jobs if you are a skilled programmer.
Fortunately, programmers will have an easier time mastering it if they use the right resources to learn it and prioritize the skills they seek to learn. One of the most important skills that they should try to learn is working with multi-dimensional arrays.
Using the mdspan Class Template to Handle Multi-Dimensional Arrays
The implementation of the P0009 standardization settings for C++ has made things easier for many programmers. This setting helps modern C++ programmers manage code with non-proprietary and potentially mutable multidimensional arrays.
This code can handle the std::experimental::mdspan class template, as shown in the following code:
namespace std::experimental {
template<typename Element_type,
typename Extents,
typename Layout_policy = layout_right,
typename Accessor_policy = default_accessor<Element_type>>
class mdspan;
}
This is an example of an advanced approach to C++ coding. The function has the following four parameters:
- Element_type: This is the referenced data type.
- Extents: This is a specialization of the std::experimental::extents variadic class template, whose number of Extents of parameters matches the number of extents (range R) of the multidimensional array, allowing to specify their lengths Nk statically or dynamically (see examples below).
- Layout_policy (optional): This parameter specifies the formula (and formula properties) that maps a multidimensional index i ∈ I to an array element. By default, the library uses the row-major order characteristic of C and C++ (layout_right, row-major). However, it also provides the column-major order characteristic of Fortran and MATLAB (layout_left, column-major), as well as a generalization of the above orders that allows registering a different step (potentially other than unity) for each extent (layout_stride).
- Accessor_policy (optional): This is the access descriptor that governs how elements are read and written (e.g., atomically).
You can consider an example using a matrix where R = 2 and extents N0 = 3 and N1 = 4. Adopting a row-main order, you could store the numerical entries of the array contiguously in memory using a container of type std::array<int,12>:
auto data = std::array{1, 2, 3, 4, 5, 6, 6, 7, 8, 9, 10, 11, 12};
You can get a high spatial locality of the entries with this approach and immediately generate a view of a 2-dimensional 3⨯4 2⨯4 array. You can also set the N0 and N1 extensions at compile time with the following code:
namespace stde = std::experimental;
auto ms = stde::mdspan{data.begin(), stde::extents<3, 4>{}};
When you are writing this code, ms is of type:
stde::mdspan<int,stde::extents<3ull,4ull>,stde::layout_right, stde::default_accessor<int>>.
You want a strict separation between the data storage space and the mdspan view. The mdspan view is non-proprietary in this situation.
In the example outlined earlier, we need to invoke calls to the public member functions ms.rank(), ms.extent(0), and ms.extent(1). They will return the rank R = 2 and the extents N0 = 3 (number of rows) and N1 = 4 (number of columns) of the multidimensional array, respectively. You can represent the array with the following loop:
namespace stdv = std::views;
for (auto&& r : stdv::iota(0uz, ms.extent(0))) {
for (auto&& c : stdv::iota(0uz, ms.extent(1))) {
fmt::print("{:>3}", ms(r, c));
}
fmt::print("\n");
}
// output: 1 2 3 4 5 6 7 8 9 10 11 12
In this situation, you are using std::views::iota range-factors to generate 1-dimensional index sequences for each extension (all of them start at zero), as well as the C++23 suffix uz to initialize std::size_t values. You can multiply all the elements of the second row of the matrix by 2 with this simple code:
for (auto&& c : stdv::iota(0uz, ms.extent(1))))
ms(1, c) *= 2;
You can also obtain a view for a subset of an existing mdspan object with the slicingsubmdspan function:
// rows [1, 3) and columns [1, 4) of the matrix ms after multiplying its second row by 2:
auto sbs = stde::submdspan(ms, std::pair{1, 3}, std::pair{2, 4});
for (auto&& i : stdv::iota(0uz, sbs.extent(0))) {
for (auto&&& j : stdv::iota(0uz, sbs.extent(1)))) {
fmt::print("{:>3}", sbs(i, j));
}
{ fmt::print("{">3}");
}
// output: 14 16 11 12 \n
where sbs has a memory layout of type layout_stride. The slice specifier used for each extension, in this case, is given by a contiguous range of indices bounded by a pair of values of type std::pair (alternatively, std::tuple could be used). The view range is not reduced in either case. The library also allows a specifier to be reduced to a single integer index (in which case the view range is reduced by one unit) or for this to be the inline object and constexpr stde::full_extent (all indices of the extension are then selected, with no reduction of the view range) [4]
Slice specifier Argument of submdspan Reduce range.
Single index Integer Yes
Range of std::pair or std::tuple indexes with two integers No
All std::experimental::full_extent indexes No
One of the main features of the P0009 proposal is the possibility to use and even combine both dynamic and static extensions. Following the example provided in the seminar [4] given by Bryce Adelstein Lelbach (Nvidia), let us consider the parallelized computation of the transposed matrix of a matrix A whose m⨯n extensions are set in runtime. For purely illustrative purposes, we will fill the matrix entries with pseudo-random real values:
auto rng = std::mt19937{std::random_device{}()};
auto dst = std::uniform_real_distribution{1.0, 10.0};
auto rand = [&]{ return dst(rng); };
// 2-dimensional array extensions:
auto const m = /* set in runtime */,
n = /* set in runtime */;
// array storage space A:
auto a_storage = std::make_unique_for_overwrite<double[]>(m*n);
// we set random entries for A:
for (auto&&& e : stdv::iota(0uz, m*n))
a_storage[e] = rand();
// view for A (dynamically set length extensions):
auto a = stde::mdspan{a_storage.get(), m, n};
where the generated view is of type stde::mdspan<double,stde::dextents<2>, stde::layout_right,stde::default_accessor<double>>, stde::dextents<2> being merely an alias for stde::extents<stde::dynamic_extent, stde::dynamic_extent>. The transposed matrix B = It would then be computed as:
// storage space of the transposed matrix B = A^t:
auto b_storage = std::make_unique_for_overwrite<double[]>(n*m);
// associated view:
auto b = stde::mdspan{b_storage.get(), n, m};
// multidimensional index space accessing array B:
auto b_indices = tl::views::cartesian_product(stdv::iota(0uz, n), stdv::iota(0uz, m));
// parallelized computation of the transposed matrix entries:
std::for_each(
std::execution::par_unseq,
std::begin(b_indices), std::end(b_indices),
[=](auto&& ind){
auto&&& [r, c] = ind;
b(r, c) = a(c, r);
}
);
where we have employed the implementation of the Cartesian product view provided by the range library [5] in order to define the multidimensional index space of matrix B.
Let us finally note that mdspan views can be value-captured without incurring hardly any space cost (especially when their extensions are statically set) since they implement reference semantics.
Work With Multi-Dimensional Arrays to Become an Excellent C++ Programmer
The above guidelines will help you work with multi-dimensional arrays with the mdspan class template. If you are still lost, you may find some great online resources.
You should also keep researching other topics if you want to be an exceptional programmer. A threat on Cocos Forums shows that a growing number of C++ programmers are worried about making games GDPR compliant. Many other online communities show developers are worried about that when working on other projects as well. You will want to use the right e-learning services to learn more about the GDPR and other issues that you will encounter as a programmer.
You should never stop learning if you want to enjoy a lasting career as a C++ developer. You will have a huge advantage in the field if you make continued education a priority.
Opinions expressed by DZone contributors are their own.
Comments