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 |
yes |
yes |
no (only 1D and 2D) |
Non-owning view of data |
yes, via |
yes, via |
yes, via |
yes, via |
Compile-time dim size |
no |
yes, via template parameters |
no |
yes, via |
Array values (owning data) |
yes, via |
yes? (planned for |
yes, via |
yes, via |
Value semantic (Regular) |
yes, via cctor, mctor, assign, massign, auto decay of views |
yes? (planned for |
partial, assigment on equal extensions |
yes (?) |
Move semantic |
yes, via mctor and massign |
yes? for |
no (C++98 library) |
yes (?) |
const-propagation semantics |
yes, via |
no, const mdspan elements are assignable! |
no, inconsistent |
(?) |
Element initialization |
yes, via nested init-list |
no (?) |
no |
no, only delayed init via |
References w/no-rebinding |
yes, assignment is deep |
no, assignment of mdspan rebinds! |
yes |
yes (?) |
Element access |
yes, via |
yes, via |
yes, via |
yes, via |
Partial element access |
yes, via |
no, only via |
yes, via |
yes, via |
Subarray views |
yes, via |
yes, via |
yes, via |
yes, via |
Subarray with lower dim |
yes, via |
yes, via |
yes, via |
yes, via |
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, |
no |
no |
no? |
Expression templates |
no (implementable through Restrictions, see section) |
no |
no |
yes (via operator overloading |
Custom Allocators |
yes, via |
yes(?) through `mdarray’s adapted container |
yes (stateless?) |
no |
PMR Allocators |
yes, via |
yes(?) through `mdarray’s adapted container |
no |
no |
Fancy pointers / references |
yes, via |
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 |
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 |
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 |
(?) , experience from Kokkos incarnation |
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 |
|
|
Initialization (2 elements) |
|
|
Element assignment |
|
|
Element access (print 2nd) |
|
|
Initialization |
|
|
In the more general case for the dimensionality, we have the following correspondence:
| FORTRAN | C++ Multi | |
|---|---|---|
Construction 2D (3 by 3) |
|
|
Construction 2D (2 by 2) |
|
|
Construction 1D (3 elements) |
|
|
Assign the 1st column of A2D |
|
|
Assign the 1st row of A2D |
|
|
Assign upper part of A2D |
|
|
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.