Reference
Fundamental Types and Concepts
The library interface presents several closely related types (C++ classes) representing arrays.
The fundamental types represent multidimensional containers (called array), references that can refer to subsets of these containers (called subarray), and iterators.
In addition, there are other classes for advanced uses, such as multidimensional views of existing buffers (called array_ref) and non-resizable owning containers (called dynamic_array).
If you want to read the API interface as coded, please visit the API documentation page.
Summary (Cheat Sheet)
Here is a table to summarize the semantic properties of each class.
The main array type to reach for is multi::array, the other types are for special uses.
multi::subarray<T, D> are generally not used explicitly.
| Type | Dimensionality | Sizes | Memory | Ownership | Uses |
|---|---|---|---|---|---|
|
Static |
Dynamic/Mutable |
Heap (allocates) |
Yes, transferable (movable) |
General purpose (value type) |
|
Static |
Dynamic/Immutable |
Any (doesn’t allocate) |
No, (pinned) |
Use existing buffers (e.g. from other libs) |
|
Static |
Dynamic/Mutable (bounded) |
Stack (doesn’t heap-allocate) |
Yes, non-transferable (pinned) |
Fast, stack constrained, lowest level of divide and conquer |
|
Static |
Dynamic/Immutable |
Heap (allocates, never reallocates) |
Yes, non-transferable (pinned) |
Multithreading, prevent invalidation |
|
Static |
Dynamic/Immutable |
Any (doesn’t allocate) |
No, (pinned) |
As reference, Implicitly used through |
When using the library, it is simpler to start from multi::array, and other types are rarely explicitly used, especially if using the auto language feature;
however, it is convenient for documentation to present the classes in a different order since the classes multi::array, multi::dynamic_array, multi::array_ref, and subarray have an is-a relationship (from left to right), through C++ public inheritance.
For example, multi::array_ref has all the methods available to multi::subarray, and multi::array has all the operations of multi::array_ref, etc.
Subarrays
A subarray-reference is part (or a whole) of another larger array, and they are represented by multi::subarray in the library.
It is important to understand that subarray s have referential semantics, their elements are not independent of the values of the larger arrays they are part of.
An instance of this class represents a subarray with elements of type T and dimensionality D, stored in memory described by the pointer type P.
(T, D, and P initials are used in this sense across the documentation.)
Instances of this class have reference semantics and behave like "language references" as much as possible.
As references, they cannot be rebound or resized; assignments are always "deep".
They are characterized by a size that does not change in the lifetime of the reference.
They are usually the result of indexing over other multi::subarray 's and multi::array 's objects, typically of higher dimensions;
therefore, the library doesn’t expose constructors for this class.
The whole object can be invalidated if the original array is destroyed.
(Additionally, multi::const_subarray provides a similar interface to multi::subarray but it protects referenced elements from being modified.)
All member functions are constexpr unless specified otherwise.
The class contains the usual interface of a standard container (e.g. std::vector): value_type, reference, const_reference, size_type, front, back, begin, end, difference_type, iterator, const_iterator, operator[] etc., that take into account the properties of the containers in the leading dimension.
In addition it has multidimensional characteristics, such as extents_type, sizes_type, multidimensional operator() and operator[], dimensionality, sizes, etc.
The special case of D == 1 has the pointer and const_pointer.
The class doesn’t expose a copy-constructor.
It is important to note that assignments in this library are always "deep," and reference-like types cannot be rebound after construction.
Assignment is never a move assignment (there is no owning pointer to steal), so it has O(N) cost, same as the swap operation.
All relational operators ==, <, etc. are provided.
It is important to note that, in this library, comparisons are also always "deep".
Lexicographical order is defined recursively, starting from the first dimension index and from left to right.
For example, A < B if A[0] < B[0], or A[0] == B[0] and A[1] < B[1], or …, etc.
Lexicographical order applies naturally if the extents of A and B are different; however, their dimensionalities must match.
See the sort examples.
Regarding multidimensional indexing:
- subarray::operator()(i, j, k, …), as in S(i, j, k) for indices i, j, k is a synonym for A[i][j][k], the number of indices can be lower than the total dimension (e.g., S can be 4D).
Each index argument lowers the dimension by one.
- subarray::operator()(ii, jj, kk), the arguments can be indices or ranges of indices (index_range member type).
This function allows positional-aware ranges.
Each index argument lowers the rank by one.
A special range is given by multi::_, which means "the whole range" (also spelled multi::all).
For example, if S is a 3D of sizes 10-by-10-by-10, S(3, {2, 8}, {3, 5}) gives a reference to a 2D array where the first index is fixed at 3, with sizes 6-by-2 referring the subblock in the second and third dimension.
Note that S(3, {2, 8}, {3, 5}) (6-by-2) is not equivalent to S[3]({2, 8})({3, 5}) (2-by-10).
- operator()() (no arguments) gives the same array but always as a subarray type (for consistency), S() is equivalent to S(S.extent()) and, in turn to S(multi::_) or S(multi::all).
Some member functions are generally used for accessing details of the internal data structure (layout) interfacing with C-libraries, layout, base, stride, and strides.
Iterators in the leading dimension are created by begin and end.
Views that affect the leading dimension are dropped, chunked, taked, element_transformed, and partitioned.
Accessing transposed and rotated views is also possible.
Flat access to the array can be obtained with elements().
Refer to the API for a complete list of member functions and types.
A reference subarray can be invalidated when its origin array is invalidated or destroyed.
For example, if the array from which it originates is destroyed or resized.
Array References
An array reference, or D-dimensional view of a contiguous pre-existing memory buffer, is represented by an object of type multi::array_ref<T, D, P = T*>.
This class doesn’t manage the elements it contains, and it has reference semantics (it can’t be rebound, assignments are deep, and have the same size restrictions as subarray)
Since array_ref is-a subarray, it inherits all the class methods and types described before and, in addition, it defines these members below.
In addition, it can be constructed from a raw pointer to access memory that is external to the library: array_ref::array_ref({e1, e2, …}, p).
Destructor is trivial since elements are not owned or managed.
The interface is almost the same as for subarray, which allows to generically program for both subarray and array_ref.
array_ref contains some optimizations; for example, elements provides a flattened random-access and contiguous view of all the elements in the array in canonical order.
An array_ref can be invalidated if the original buffer is deallocated.
Dynamic Arrays
A dynamic array is a D-dimensional array that manages an internal memory buffer, and it is represented by multi::dynamic_array<T, D, Alloc = std::allocator<T>>.
This class owns the elements it contains; it has restricted value semantics because assignments are restricted to sources with equal sizes.
Memory is requested by an allocator of type Alloc, the standard allocator by default.
It supports stateful and polymorphic allocators, which are the default for the special type multi::pmr::dynamic_array.
For most uses, a multi::array should be preferred instead.
The main feature of this class is that its iterators, subarrays, and pointers do not get invalidated unless the whole object is destroyed.
In this sense, it is semantically similar to a C-array, except that elements are allocated from the heap.
It can be useful for scoped uses of arrays and multithreaded programming and to ensure that assignments do not incur allocations.
The C++ core guidelines proposed a similar (albeit one-dimensional) class, called gsl::dyn_array.
This class has a constructor that allocates memory given the desired sizes: dynamic_array::dynamic_array({e1, e2, …}, T val = {}, Alloc = {}) constructs a D-dimensional array by allocating elements. dynamic_array::dynamic_array(std::initializer_list<…>) constructs the array with elements initialized from a nested list.
The destructor deallocates memory and destroys the elements
operator= assigns the elements from the source (sizes must match).
In matters that are not related to memory ownership or allocation, dynamic_array has the same interface as multi::array_ref.
Arrays
This is the main class in the library.
An array of integer positive dimension D has value semantics if its element type T has value semantics, and it is represented by multi::array<T, D, Alloc = std::allocator<T>>.
It supports stateful and polymorphic allocators, which is implied for the special type multi::pmr::array<T, D>.
It inherits the behavior interface (including constructors) of dynamic_array, except that array has full-value semantics and can reallocate memory; therefore
operator= assigns from an arbitrary array (or subarray-reference) subarray, or from another array. array`s enjoy move semantics and `swap is O(1).
reextent changes the size of the array to new extents, reextent({e1, e2, …}), while elements are preserved when possible.
New elements are initialized with a default value v with a second argument reextent({e1, e2, …}, v).
The first argument is of extents_type, and the second is optional for element types with a default constructor.
Finally multi::inplace_array is a special kind of multi::array that uses a internal stack-based allocator; this guarantees total interface compatibility with multi::array (it owns the elements, it is resizable, has iterators, etc.)
The third template argument of multi::inplace_array<T, D, MaxCapacity> is the maximum capacity of the array (not an allocator.).
The number of elements of the array is dynamic as long as the number of elements is less or equal than MaxCapacity (the behavior is undefined otherwise.)
Because it stack allocated it doesn’t have O(1) moves (move falls back to copy), although copies are fast because memore is in the stack.
It is very efficient to small arrays of known maximum size.
Restrictions
A restriction is an array that generates its elements lazily on the fly. It is defined by combining a D-dimensional function with a D-dimensional extents object. The function is therefore restricted to this Cartesian grid. The defining property of restrictions in this library is that they provide the same generic interface as an immutable array. This includes the iteration and element-access interface, although pointer access and layout access are not, since there is no actual memory as its elements are not stored.
A factory function multi::restricted(F fun, multi::extensions_t<D> exts) can be used to generate a restriction object.
fun must be a function that returns deterministically (e.g. doesn’t depend on the order of evaluation);
this is guaranteed if F fulfills the "regular invocable" concept (std::regular_invocable<F, Args…> is true and sizeof…(Args) is the number of dimensions D.)
It has almost all properties of array (or subarray) except that there is no memory backing up the elements.
So for example, it has value_type, reference, const_reference, size_type, extents_type, difference_type, operator[], iterator, const_iterator, elements, begin, and end.
Since restrictions have the same interface as array, they can be manipulated as if they were concrete arrays, except that elements cannot be modified (they are the result of a function evaluation).
Finally, these lazy arrays are generated by functions in the elementwise namespace, such as multi::elementwise::operator+.
Elementwise operations are available through the inclusion of the multi/elementwise.hpp header.
Iterators
The library offers random-access iterators to subarrays of dimension D - 1, and they are represented by types of the form multi::[sub]array<T, D, P>::(const_)iterator.
These are generally used to interact with or implement algorithms.
They can be default constructed but do not expose other constructors since they are generally obtained from begin or end, manipulated arithmetically, operator--, operator+` (pre and postfix), or random jumps `operator/operator- and operator+=/operator-=.
They can be dereferenced by operator* and index access operator[], returning objects of lower dimension subarray<T, D, … >::reference (see above).
Note that this is the same type for all related arrays, for example, multi::array<T, D, P >::(const_)iterator.
iterator can be invalidated when its original array is invalidated, destroyed or resized.
An iterator that stems from dynamic_array becomes invalid only if the original array was destroyed (e.g. out-of-scope).
Both concrete arrays (and subarrays) and restrictions have iterators (.begin() and .end() member functions)
Cursors
A cursor is a pointer-like type that supports direct multidimensional indexing from a fixed origin.
It is represented by multi::cursor_t<ElementPtr, D, StridesType> and obtained via the .home() member of any array or subarray, which returns a cursor pointing to the top-corner element at index [0, 0, …, 0].
Unlike iterators, which are one-dimensional and step through D-1-dimensional subarrays, a cursor carries the full stride information for all D dimensions and can reach any element in a single indexing expression.
Chained operator[] notation steps through dimensions one at a time:
auto c = A.home();
auto& elem = c[i][j][k]; // equivalent to A[i][j][k]
Cursor objects are cheap (and trivial) to copy. Cursors are particularly useful in GPU and SIMD contexts where direct pointer arithmetic over strided memory is more natural than iterator-based traversal.
In some loose sense, cursors are like array-references that "forgot" their sizes, they are a way to pass arrays to interfaces that are restricted to take object by value (such as CUDA kernels or C functions)
Both concrete arrays (and subarrays) and restrictions have cursors (.home() member function).
Type Requirements
The library design tries to impose the minimum possible requirements over the types that parameterize the arrays. Array operations assume that the contained type (element type) is regular (i.e. different elements represent disjoint entities that behave like values). Pointer-like random access types can be used as substitutes of built-in pointers. Therefore, pointers to special memory and fancy-pointers are supported.
Linear Sequences: Pointers
An array_ref can reference an arbitrary random access linear sequence (e.g. memory block defined by pointer and size).
This way, any linear sequence (e.g. raw memory, std::vector, std::queue) can be efficiently arranged as a multidimensional array.
std::vector<double> buffer(100);
multi::array_ref<double, 2> A({10, 10}, buffer.data());
A[1][1] = 9.0;
assert( buffer[11] == 9.0 ); // the target memory is affected
Since array_ref does not manage the memory associated with it, the reference can dangle if the buffer memory is reallocated (e.g. by vector-resize in this case).
Special Memory: Pointers and Views
array 's manage their memory behind the scenes through allocators, which can be specified at construction.
It can handle special memory, as long as the underlying types behave coherently, these include fancy pointers (and fancy references).
Associated fancy pointers and fancy reference (if any) are deduced from the allocator types.
Allocators and Fancy Pointers
Specific uses of fancy memory are file-mapped memory or interprocess shared memory.
This example illustrates memory persistency by combining with Boost.Interprocess library.
The arrays support their allocators and fancy pointers (boost::interprocess::offset_ptr).
#include <boost/interprocess/managed_mapped_file.hpp>
using namespace boost::interprocess;
using manager = managed_mapped_file;
template<class T> using mallocator = allocator<T, manager::segment_manager>;
decltype(auto) get_allocator(manager& m) {return m.get_segment_manager();}
template<class T, auto D> using marray = multi::array<T, D, mallocator<T>>;
int main() {
{
manager m{create_only, "mapped_file.bin", 1 << 25};
auto&& arr2d = *m.construct<marray<double, 2>>("arr2d")(marray<double, 2>::extensions_type{1000, 1000}, 0.0, get_allocator(m));
arr2d[4][5] = 45.001;
}
// imagine execution restarts here, the file "mapped_file.bin" persists
{
manager m{open_only, "mapped_file.bin"};
auto&& arr2d = *m.find<marray<double, 2>>("arr2d").first;
assert( arr2d[7][8] == 0. );
assert( arr2d[4][5] == 45.001 );
m.destroy<marray<double, 2>>("arr2d");
}
}
(See also, examples of interactions with the CUDA Thrust library to see more uses of special pointer types to handle special memory.)
Transformed Views
Another kind of use of the internal pointer-like type is to transform underlying values.
These are useful to create "projections" or "views" of data elements.
In the following example a "transforming pointer" is used to create a conjugated view of the elements.
In combination with a transposed view, it can create a hermitian (transposed-conjugate) view of the matrix, without copying elements.
We can adapt the library type boost::transform_iterator to save coding, but other libraries can be used also.
The hermitized view is read-only, but with additional work, a read-write view can be created (see multi::hermitized in multi-adaptors).
constexpr auto conj = [](auto const& c) {return std::conj(c);};
template<class T> struct conjr : boost::transform_iterator<decltype(conj), T*> {
template<class... As> conjr(As const&... as) : boost::transform_iterator<decltype(conj), T*>{as...} {}
};
template<class Array2D, class Complex = typename Array2D::element_type>
auto hermitized(Array2D const& arr) {
return arr
.transposed() // lazily transposes the array
.template static_array_cast<Complex, conjr<Complex>>(conj) // lazy conjugate elements
;
}
int main() {
using namespace std::complex_literals;
multi::array<std::complex<double>, 2> A = {
{ 1.0 + 2.0i, 3.0 + 4.0i},
{ 8.0 + 9.0i, 10.0 + 11.0i}
};
auto const& Ah = hermitized(A);
assert( Ah[1][0] == std::conj(A[0][1]) );
}
To simplify this boilerplate, the library provides the .element_transformed(F) method that will apply a transformation F to each element of the array.
In this example, the original array is transformed into a transposed array with duplicated elements.
multi::array<double, 2> A = {
{1.0, 2.0},
{3.0, 4.0},
};
auto const scale = [](auto x) { return x * 2.0; };
auto B = + A.transposed().element_transformed(scale);
assert( B[1][0] == A[0][1] * 2 );