Skip to content

A Gentle Introduction to mdspan

David Hollman edited this page Aug 5, 2019 · 23 revisions

ISO-C++ standard proposals are not the best tutorials for how to use their proposed features. This is intentional—there's enough content to be written in exploring the technical design issues that a tutorial-style introduction is out of place in such documents. However, it also means that proposals such as P0009 for mdspan reach a level of maturity where they are ready for production implementation and use, but without a single tutorial-style introduction. Here's an attempt to do so for mdspan.

What is mdspan?

In it's simplest form, std::mdspan1 is a trivial extension of std::span:

int* data = /* ... */

// View data as contiguous memory representing 4 ints
auto s = std::span<int, 4>(data);

// View data as contiguous memory representing 2 rows
// of 2 ints each
auto ms = std::mdspan<int, 2, 2>(data);

Introductions to std::span can be found all over the internet (it will be in C++20), so we won't discuss it any further here. This tutorial assumes the reader is at least familiar with std::span in a very basic sense.

Dynamic extents

Just like span, you can create a dynamically sized mdspan using the std::dynamic_extent sentinel value:

int* data = /* ... */
int size = /* ... */

auto s = std::span<int, std::dynamic_extent>(data, size);

int rows = /* ... */
int cols = /* ... */
auto ms =
  std::mdspan<int, std::dynamic_extent, std::dynamic_extent>(data, rows, cols);

As you can see, this can get to be quite verbose very quickly. Like many similar standard-library facilities, the intent is that users should address this with type aliases and alias templates with whatever names are most sensible in a particular domain:

// existing practice with existing library features:
template <class T>
using my_pool_alloc_vector = std::vector<T, my_pool_allocator<T>>;

// similar use with mdspan:
template <class T>
using dyn_span_2d = std::mdspan<T, std::dynamic_extent, std::dynamic_extent>;
template <class T>
using dyn_span_8d =
  std::mdspan<T,
    std::dynamic_extent, std::dynamic_extent,
    std::dynamic_extent, std::dynamic_extent,
    std::dynamic_extent, std::dynamic_extent,
    std::dynamic_extent, std::dynamic_extent
  >;
using particle_positions_view = std::mdspan<double, std::dynamic_extent, 3>;

We will, obviously, continue to use the canonical form in this document, but keep in mind that the verbosity of actual code in your domain may have significantly less verbosity. It is also possible that some helper alias templates will be added to the standard in the future, but the committee will probably wait one cycle at least to see what developes as common practice.

Construction

As alluded to above, in its simplest form mdspan is constructed just like span, but with more sizes. The simplest form of the constructor takes a pointer (T* for mdspan<T, ...>) and a variadic pack of dynamic sizes:

template <class... Integer>
constexpr explicit
basic_mdspan(pointer ptr, Integer... sizes);

Element access

Accessing elements of an mdspan with more than one dimension uses the call operator, (), because the [] operator cannot take multiple indices:2

void apply_rotation(
  std::mdspan<double, 3, 3> rotation,
  std::mdspan<double, 3> vec,
  std::mdspan<double, 3> out
)
{
  for(int i = 0; i < 3; ++i) {
    out(i) = 0;
    for(int j = 0; j < 3; ++j) {
      out(i) += rotation(i, j) * vec(j);
    }
  }
}

Note that out[i] and vec[j] are perfectly acceptable as well, but rotation[i, j] is currently ill-formed. To get the extents for a particular dimension of an mdspan, you use the extent() method

void apply_matrix_transformation(
  std::mdspan<double, std::dynamic_extent, std::dynamic_extent> mat,
  std::mdspan<double, std::dynamic_extent> vec,
  std::mdspan<double, std::dynamic_extent> out
)
{
  assert(mat.extent(1) == vec.extent(0));
  assert(mat.extent(0) == out.extent(0));
  for(int i = 0; i < mat.extent(0); ++i) {
    out(i) = 0;
    for(int j = 0; j < mat.extent(1); ++j) {
      out(i) += rotation(i, j) * vec(j);
    }
  }
}

Layout and access customization

The design of mdspan addresses a much broader variety of needs than span. In many scenarios, it even makes more sense to use a one-dimensional mdspan instead of a span. In the proposal, mdspan is actually just an alias template for a more general-purpose class template, basic_mdspan (by analogy to string/basic_string, for instance):

template <
  class T,
  class Extents,
  class LayoutPolicy=std::layout_right,
  class Accessor=std::accessor_basic
>
class basic_mdspan;

template <class T, ptrdiff_t... Extents>
using mdspan = basic_mdspan<T, std::extents<Extents...>>;

The third and fourth template parameters are customizations for data layout (i.e., turning a bunch of indices into an offset) and data access (e.g., turning a pointer into a reference) respectively. You can think of these customizations like the same kind of thing as the Hash template parameter on std::unordered_map or the Allocator template parameter on std::vector. These are things that most of the time you don't have to touch, and that most algorithms shouldn't have to care about—how many times have you written a function that takes a std::vector and had to write a different version of the function for different allocators? (Probably not that often, if ever.)

A more advanced tutorial will cover the topics of layout and access customization, but for now, it's sufficient to know that two simple ones are provided: std::layout_right and std::layout_left, with the right-most and left-most indices are fast-running, respectively. When discussing matrices, these are often described as C-style layout (or row-major) and FORTRAN-style (or column-major), respectively.

Footnotes

1 We'll use the namespace std in this document, since that is eventually where mdspan will end up, but as is the case with all implementations of standards proposals, the actual implementation is in namespace std::experimental until the proposal is accepted. It's a common convention among users of these features to put namespace stdex = std::experimental somewhere in their project to reduce verbosity.
2 This may be changing. See first steps in P1161, which was accepted into C++20; mdspan will change accordingly if the corresponding change makes it into C++23 early enough.
Clone this wiki locally