Appendix

Comparison to other array libraries (mdspan, Boost.MultiArray, etc)

The C++23 standard provides std::mdspan, a non-owning multidimensional array. So here is an appropriate point to compare the two libraries. Although the goals are similar, the two libraries differ in their generality and approach.

The Multi library concentrates on well-defined value- and reference-semantics of arbitrary memory types with regularly arranged elements (distributions described by strides and offsets) and extreme compatibility with STL algorithms (via iterators) and other fundamental libraries. While mdspan concentrates on arbitrary layouts for non-owning memory of a single type (CPU raw pointers). Due to the priority of arbitrary layouts, the mdspan research team didn’t find efficient ways to introduce iterators into the library. Therefore, its compatibility with the rest of the STL is lacking. Preliminarily, Multi array can be converted (viewed as) mdspan.

Boost.MultiArray is the original multidimensional array library shipped with Boost. This library can replace Boost.MultiArray in most contexts, it even fulfills the concepts of boost::multi_array_concepts::ConstMultiArrayConcept and …​::MutableMultiArrayConcept. Boost.MultiArray has technical and semantic limitations that are overcome in this library, regarding layouts and references; it doesn’t support value-semantics, iterator support is limited, and it has other technical problems.

Eigen is a very popular matrix linear algebra framework library, and as such, it only handles the special 2D (and 1D) array case. Instead, the Multi library is dimension-generic and doesn’t make any algebraic assumptions for arrays or contained elements (but still can be used to implement, or in combination, with dense linear algebra algorithms.)

Other frameworks includes the OpenCV (Open Computing Vision) framework, which is too specialized to make a comparison here.

Here is a table comparing with mdspan, R. Garcia’s Boost.MultiArray and Eigen. (online).

Multi mdspan/mdarray Boost.MultiArray (R. Garcia) Inria’s Eigen

No external Deps

yes (only Standard Library C++17)

yes (only Standard Library C17/C26)

yes (only Boost)

yes

Arbitrary number of dims

yes, via positive dimension (compile-time) parameter D

yes

yes

no (only 1D and 2D)

Non-owning view of data

yes, via multi::array_ref<T, D>(ptr, {n1, n2, …​, nD})

yes, via mdspan m{T*, extents{n1, n2, …​, nD}};

yes, via boost::multi_array_ref<T, D>(T*, boost::extents[n1][n2]…​[nD])

yes, via Eigen::Map<Eigen::Array<T, Eigen::Dynamic, Eigen::Dynamic>>(ptr, n1, n2)

Compile-time dim size

no

yes, via template parameters mdspan{T*, extent<16, dynamic_extents>{32} }

no

yes, via Eigen::Array<T, N1, N2>

Array values (owning data)

yes, via multi::array<T, D>({n1, n2, …​, nD})

yes? (planned for mdarray)

yes, via boost::multi_array<T, D>(boost::extents[n1][n2]…​[nD])

yes, via Eigen::Array<T>(n1, n2)

Value semantic (Regular)

yes, via cctor, mctor, assign, massign, auto decay of views

yes? (planned for mdarray)

partial, assigment on equal extensions

yes (?)

Move semantic

yes, via mctor and massign

yes? for mdarray (depends on adapted container)

no (C++98 library)

yes (?)

const-propagation semantics

yes, via const or const&

no, const mdspan elements are assignable!

no, inconsistent

(?)

Element initialization

yes, via nested init-list

no (?)

no

no, only delayed init via A << v1, v2, …​;

References w/no-rebinding

yes, assignment is deep

no, assignment of mdspan rebinds!

yes

yes (?)

Element access

yes, via A(i, j, …​) or A[i][j]…​

yes, via A[i, j, …​]

yes, via A[i][j]…​

yes, via A(i, j) (2D only)

Partial element access

yes, via A[i] or A(i, multi::all)

no, only via submdspan(A, i, full_extent)

yes, via A[i]

yes, via A.row(i)

Subarray views

yes, via A({0, 2}, {1, 3}) or A(1, {1, 3})

yes, via submdspan(A, std::tuple{0, 2}, std::tuple{1, 3})

yes, via A[indices[range(0, 2)][range(1, 3)]]

yes, via A.block(i, j, di, dj)

Subarray with lower dim

yes, via A(1, {1, 3})

yes, via submdspan(A, 1, std::tuple{1, 3})

yes, via A[1][indices[range(1, 3)]]

yes, via A(1, Eigen::placeholders::all)

Subarray w/well def layout

yes (strided layout)

no

yes (strided layout)

yes (strided)

Recursive subarray

yes (layout is stack-based and owned by the view)

yes (?)

no (subarray may dangle layout, design bug?)

yes (?) (1D only)

Restrictions (lazy arrays)

yes, [](auto is…​) {…​} ^ extensions_t<D>(…​)

no

no

no?

Expression templates

no (implementable through Restrictions, see section)

no

no

yes (via operator overloading A + B * C)

Custom Allocators

yes, via multi::array<T, D, Alloc>

yes(?) through `mdarray’s adapted container

yes (stateless?)

no

PMR Allocators

yes, via multi::pmr::array<T, D>

yes(?) through `mdarray’s adapted container

no

no

Fancy pointers / references

yes, via multi::array<T, D, FancyAlloc> or views

no

no

no

Stride-based Layout

yes

yes

yes

yes

Fortran-ordering

yes, only for views, e.g. resulted from transposed views

yes

yes.

yes

Zig-zag / Hilbert ordering

no

yes, via arbitrary layouts (no inverse or flattening)

no

no

Arbitrary layout

no

yes, possibly inefficient, no efficient slicing

no

no

Flattening of elements

yes, via A.elements() range (efficient representation)

yes, but via indices roundtrip (inefficient)

no, only for allocated arrays

no, not for subblocks (?)

Iterators

yes, standard compliant, random-access-iterator

no

yes, limited

no

Multidimensional iterators (cursors)

yes (experimental)

no

no

no

STL algorithms or Ranges

yes

no, limited via std::cartesian_product

yes, some do not work

no

Compatibility with Boost

yes, serialization, interprocess (see below)

no

no

no

Compatibility with Thrust or GPUs

yes, via flatten views (loop fusion), thrust-pointers/-refs

no

no

no

Used in production

QMCPACK, INQ

(?) , experience from Kokkos incarnation

yes (?)

yes

Multi for FORTRAN programmers

This section summarizes simple cases translated from FORTRAN syntax to C using the library. The library strives to give a familiar feeling to those who use multidimensional arrays in FORTRAN. Arrays can be indexed using square brackets or parenthesis, which would be more familiar to FORTRAN syntax. The most significant differences are that array indices in FORTRAN start at `1`, and that index ranges are specified as closed intervals, while in Multi, they start by default at `0`, and ranges are half-open, following C conventions. Like in FORTRAN, arrays are not initialized automatically for simple types (e.g., numeric); such initialization needs to be explicit.

FORTRAN C++ Multi

Declaration/Construction 1D

real, dimension(2) :: numbers (at top)

multi::array<double, 1> numbers(2); (at scope)

Initialization (2 elements)

real, dimension(2) :: numbers = [ 1.0, 2.0 ]

multi::array<double, 1> numbers = { 1.0, 2.0 };

Element assignment

numbers(3) = 99.0

numbers[2] = 99.0;

Element access (print 2nd)

Print *, numbers(3)

std::cout << numbers(2) << '\n';

Initialization

DATA numbers / 10.0 20.0 /

numbers = {10.0, 20.0};

In the more general case for the dimensionality, we have the following correspondence:

FORTRAN C++ Multi

Construction 2D (3 by 3)

real*8 :: A2D(3,3) (at top)

multi::array<double, 2> A2D({3, 3}); (at scope)

Construction 2D (2 by 2)

real*8 :: B2D(2,2) (at top)

multi::array<double, 2> B2D({2, 2}); (at scope)

Construction 1D (3 elements)

real*8 :: v1D(3) (at top)

multi::array<double, 2> v1D({3}); (at scope)

Assign the 1st column of A2D

v1D(:) = A2D(:,1)

v1( _ ) = A2D( _ , 0 );

Assign the 1st row of A2D

v1D(:) = A2D(1,:)

v1( _ ) = A2D( 0 , _ );

Assign upper part of A2D

B2D(:,:) = A2D(1:2,1:2)

B2D( _ , _ ) = A2D({0, 2}, {0, 2});

Note that these correspondences are notationally logical; internal representation (memory ordering) can still be different, affecting operations that interpret 2D arrays as contiguous elements in memory.

Range notation such as 1:2 is replaced by {0, 2}, which considers both the difference in the start index and the half-open interval notation in the C++ conventions. Stride notation such as 1:10:2 (i.e., from first to tenth included, every two elements) is replaced by {0, 10, 2}. Complete range interval (single : notation) is replaced by multi::_, which can be used simply as after the declaration using multi::;. These rules extend to higher dimensionality.

Multi provides algebraic operators that are expanded elementwise, For example, a FORTRAN statement like C = A + B or C = C + A is translated as this:

using multi::broadcast::operator+;

multi::array C = A + B;
C = C + A;  // assigning to itself is ok for elementwise operations

For one-dimensional arrays, this is equivalent to :

std::transform(A.begin(), A.end(), B.begin(), A.begin(), std::plus{});  // valid for 1D arrays only

Or, in the general dimensionality the equivalent code is:

auto&&      Aelems = A.elements();
auto const& Belems = B.elements();
std::transform(Aelems.begin(), Aelems.end(), Belems.begin(), Aelems.begin(), std::plus<>{});  // valid for arbitrary dimension

or

std::ranges::transform(A.elements(), B.elements(), A.elements().begin(), std::plus<>{});  // alternative using C++20 ranges

A FORTRAN statement like C = 2.0*C is rewritten as

using multi::broadcast::operator*;

C = 2.0*C;  // self-assignment is ok because this an elementwise operation

(equivalent to std::ranges::transform(C.elements(), C.elements().begin(), [](auto const& e) { return 2.0*e; });.)

Simple loops can be mapped as well, taking into account indexing differences (C++ version on the right):

do i = 1, 5         ! for(int i = 0; i != 5; ++i) {
  do j = 1, 5       !   for(int j = 0; j != 5; ++j) {
    D2D(i, j) = 0   !     D2D(i, j) = 0;
  end do            !   }
end do              ! }

However, algorithms like transform, reduce, transform_reduce and for_each offer a higher degree of control over operations, including memory allocations if needed, and even enable parallelization, providing a higher level of flexibility. In this case, std::fill(D2D.elements().begin(), D2D.elements().end(), 0); will also work.

Thanks to Joaquín López Muñoz and Andrzej Krzemienski for the critical reading of the documentation, to Sean Parent for inspiring ideas, and especially to Matt Borland for his help to integrate the Boost infrastructure to the unit tests, and for the formatting of the documentation.