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 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; it can still be used to implement, or be combined with, dense linear algebra algorithms.
Other frameworks include the OpenCV framework, which is too specialized to make a comparison here.
Here is a table comparing with mdspan, R. Garcia’s Boost.MultiArray and Eigen.
(open with Compiler Explorer).
| Multi (this library) | mdspan/mdarray | Boost.MultiArray (R. Garcia) | Inria’s Eigen | |
|---|---|---|---|---|
No external Deps |
yes (only Standard Library C++17) |
yes (only Standard Library, C++17/C++26) |
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, assignment on equal extents |
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. resulting 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 sections) |
no |
no |
no |
Compatibility with Thrust or GPUs |
yes, via flatten views (loop fusion), thrust-pointers/-refs |
yes (ad-hoc, needs markings, no pointer-type propagation) |
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 parentheses (()), 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 90 | C++ Multi | |
|---|---|---|
Declaration/Construction 1D |
|
|
Initialization (3 elements) |
|
|
Element assignment (3rd) |
|
|
Element access (print 3rd) |
|
|
Printing all elements |
|
|
Re-initialization |
|
|
In the more general case for the dimensionality, we have the following correspondence:
| Fortran 90 | 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 |
|
|
Allocatable declaration |
|
|
Allocation |
|
|
Deallocation |
|
|
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::_, (or by 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::elementwise::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::elementwise::operator*;
C = 2.0*C; // self-assignment is ok because this is 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.
Multi for APL Programmers
| APL | C++ Multi | |
|---|---|---|
Construction 1D (3 elems) |
|
|
Construction 2D (2 by 3) |
|
|
Indexing |
|
|
row |
|
|
column |
|
|
subrange of indices |
|
|
Compress |
|
no (approx: https://godbolt.org/z/z8M6qhq5G) |
Compress last axis |
|
no |
Restructuring (elements) |
|
|
Restructuring (1D to 2D) |
|
limited (approx: |
Restructuring (2D to 2D) |
|
limited (approx: |
Catenation |
|
no (approx: https://godbolt.org/z/cKcbWTooG) |
Transposition |
|
|
Main diagonal |
|
|
Drop |
|
|
Drop (from both sides) |
|
|
Rotate (3 places left) |
|
no (approx: use |
Rotate (2 places right) |
|
no (approx: use |
Reversal |
|
|
column reversed |
|
|
inspired on: https://dl.acm.org/doi/epdf/10.1145/390015.808410
Multi for NumPy Programmers
NumPy and Multi share the conventions that matter most for translation: 0-based indexing, half-open ranges ([first, last)), and row-major (C-order) storage by default, so most indexing and slicing maps almost directly.
There are two important differences to keep in mind.
First, assignment semantics: in NumPy, B = A makes B another name for the same array (and a slice A[i:j] is a view), so an independent array requires an explicit A.copy(); in Multi, auto B = A; is a deep copy (value semantics), while a reference into existing data is requested explicitly with auto&& B = A(…);.
Second, scope: unlike NumPy, Multi is not a numerical library, so element-wise arithmetic, broadcasting, reshaping, and concatenation are not first-class operations — they are expressed with standard algorithms over .elements(), or are limited.
| NumPy | C++ Multi | |
|---|---|---|
Import / include |
|
|
Construction 1D |
|
|
Construction 2D |
|
|
Zeros (given shape) |
|
|
Uninitialized (given shape) |
|
|
Shape |
|
|
Number of dimensions |
|
|
Total number of elements |
|
|
Length of first axis |
|
|
Element access |
|
|
Row |
|
|
Column |
|
|
Slice of rows |
|
|
2D block |
|
|
Strided slice (step 2) |
|
|
Transpose |
|
|
Main diagonal |
|
|
Flatten (view) |
|
|
Reshape |
|
limited (approx: |
Iterate over rows |
|
|
Deep copy |
|
|
View / reference |
|
|
Fill with a constant |
|
|
Sum of all elements |
|
|
Element-wise |
|
no (use |
Broadcasting |
|
no (use |
Concatenation |
|
no (approx: use |
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 integrating the Boost infrastructure into the unit tests, and for the formatting of the documentation.