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).

Summary (cheat sheet)

Here is 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

multi::array<T, D, …​>

Static

Dynamic/Mutable

Heap (allocates)

Yes, transferable (movable)

General purpose (value type)

multi::array_ref<T, D>

Static

Dynamic/Immutable

Any (doesn’t allocate)

No, (pinned)

Use existing buffers (e.g. from other libs)

multi::inplace_array<T, D, …​>

Static

Dynamic/Mutable (bounded)

Stack (doesn’t heap-allocate)

Yes, non-transferable (pinned)

Fast, stack constrained, lowest level of divide and conquer

multi::dynamic_array<T, D, …​>

Static

Dynamic/Immutable

Heap (allocates, never reallocates)

Yes, non-transferable (pinned)

Multithreading, prevent invalidation

multi::subarray<T, D>

Static

Dynamic/Immutable

Any (doesn’t allocate)

No, (pinned)

As reference, Implicitly used through auto or template, prevent code bloat across compilation boundaries

When using the library, it is simpler to start from 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 array, dynamic_array, array_ref, and subarray have an is-a relationship (from left to right), through C++ public inheritance. For example, array_ref has all the methods available to subarray, and array has all the operations of `array_ref, etc.

Subarrays

A subarray-reference is part (or a whole) of another larger array, and they are represented by multi::subarray<T, D, P = T*> 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 rebinded 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.

multi::subarray<T, D, P = T*> member types

A subarray-reference of type T and non-negative dimension D (>= 0). There are no special requirements over type T. The type P must be pointer-like.

subarray::…​ Description

value_type

multi::array<T, D - 1> or, for D == 1, T (the element type)

reference

multi::subarray<T, D-1> or, for D == 1, pointer_traits<P>::reference (usually T&)

const_reference

multi::const_subarray<T, D-1, P > or, for D == 1, pointer_traits<P>::rebind<T const>::reference (usually T const&)

index

indexing type in the leading dimension (usually std::ptrdiff_t)

size_type

describe size (number of subarrays) in the leading dimension (signed version of pointer size type, usually std::diffptr_t)

index_range

describe ranges of indices, constructible from braced indices types or from an extension_type. Can be continuous (e.g. {2, 14}) or strided (e.g. {2, 14, /every/ 3})

extension_type

describe a contiguous range of indices, constructible from braced index (e.g. {0, 10}) or from a single integer size (e.g. 10, equivalent to {0, 10}).

element

Element type T

element_ref

Reference to element type, deduced from pointer type, typically T&

element_ref

Constant reference to element type, deduced from pointer type, typically T const&

difference_type

describe index differences in leading dimension (signed version of pointer size type, usually std::ptrdiff_t)

pointer

multi::subarray_ptr<T, D-1, P > or, for D == 1, P (the element pointer type, usually T*)

const_pointer

multi::const_subarray_ptr<T, D-1, P > or, for D == 1, pointer_traits<P>::rebind<T const> (usually T const*)

iterator

describe a random-access iterator in the leading dimension

const_iterator

describe a random-access iterator in the leading dimension to constant data

multi::subarray<T, D, P = T*> special member functions

subarray::…​

(constructors)

not exposed; copy constructor is not available since the instances are not copyable; destructors are trivial since it doesn’t own the elements

operator=

assigns the elements from the source; the sizes must match

swap (friend)

swaps the elements of two subarrays O(n) cost; the sizes must match

It is important to note that assignments in this library are always "deep," and reference-like types cannot be rebound after construction. (Reference-like types have corresponding pointer-like types that provide an extra level of indirection and can be rebound (just like language pointers); these types are multi::array_ptr and multi::subarray_ptr corresponding to multi::array_ref and multi::subarray respectively.)

multi::subarray<T, D, P = T*> relational functions

operator==/operator!=

Tells if elements of two subarray s are equal and if extensions of the subarrays are the same

operator</operator⇐

Less-than/less-or-equal lexicographical comparison (requires elements to be comparable)

operator>/operator>=

Greater-than/greater-or-equal lexicographical comparison (requires elements to be comparable)

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 extensions of A and B are different; however, their dimensionalities must match. (See sort examples).

multi::subarray<T, D, P = T*> shape access

dimensionality

static constant member, dimensionality of array D

sizes

returns a tuple with the sizes in each dimension

extensions

returns a tuple with the extensions in each dimension

size

returns the number of subarrays contained in the first dimension

extension

returns a contiguous index range describing the set of valid indices

num_elements

returns the total number of elements

multi::subarray<T, D, P = T*> element access

operator[]

access specified element by index (single argument), returns a reference member type (see above), for D > 1 it can be used recursively

front

access first element (undefined result if array is empty). Takes no argument.

back

access last element (undefined result if array is empty). Takes no argument.

operator()

When used with zero arguments, it returns a subarray reference representing the whole array.

operator()(i)

When used with one argument, access a specified element by index (return a reference member type) or by range (return a subarray of equal dimension).

  • 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.extension()) and, in turn to S(multi::_) or S(multi::all).

multi::subarray<T, D, P = T*> structure access

These member functions are generally used for accessing details of the internal data structure (layout) interfacing with C-libraries.

subarray::…​

layout

returns a single layout object with stride and size information

base

direct access to underlying memory pointer (S[i][j]…​ == S.base() + std::get<0>(S.strides())*i + std::get<1>(S.strides())*j + …​)

stride

return the stride value of the leading dimension, e.g (&A[1][0][0]…​ - &A[0][0]…​)

strides

returns a tuple with the strides defining the internal layout

multi::subarray<T, D, P = T*> iterators

subarray::…​

begin/cbegin

returns (const) iterator to the beginning

end/cend

returns (const) iterator to the end

multi::subarray<T, D, P = T*> subarray/array generators

These operations generate different ways to view the elements of a (sub)array, but without copying elements or allocate)

subarray::…​ (these operations do not copy elements or allocate)

broadcasted

returns a view of higher dimensionality (D + 1) obtained by infinite repetition of the original array. (This returns a special kind of subarray with a degenerate layout and no size operation. Takes no argument.)

chunked

a view of higher dimensionality resulting from partitioning the original into subarrays of a certain length

dropped

(takes one integer argument n) returns a subarray with the first n-elements (in the first dimension) dropped from the original subarray. This doesn’t remove or destroy elements or resize the original array

element_transformed

creates a view of the array, where each element is transformed according to a function (first and only argument)

elements

a flatted random-access view of all the elements rearranged canonically. A.elements()[0] → A[0][0], A.elements()[1] → A[0][1], etc. The type of the result is not a subarray but a special kind of random-access range. Takes no arguments.

rotated/unrotated

a view (subarray) of the original array with indices (un)rotated from right to left (left to right), for D = 1 returns the same subarray. For given i, j, k, A[i][j][k] gives the same element as A.rotated()[j][k][i] and, in turn the same as A.unrotated()[k][i][j]. Preserves dimension. The function is cyclic; D applications will give the original view. Takes no argument.

taked

a view of the original array with the first n values in the leading dimension

transposed (same as prefix operator~)

a view (subarray) of the original array with the first two indices exchanged, only available for D > 1; for D = 2, rotated, unrotated and transposed give same view. Takes no argument.

partitioned

a view of higher dimensionality resulting by splitting the original range in a certain number of parts (complementary to .chunked)

sliced

(takes two index arguments a and b) returns a subarray with values from index a to index b in the leading dimension (non-inclusive) {S[a], …​ S[b-1]}. Preserves the dimension.

strided

(takes one integer argument s) returns a subarray skipping s values. Preserves the dimension.

dynamic_array_cast<T2, P2 = T2*>(args…​)

produces a view where the underlying pointer constructed by P2{A.base(), args…​}. Usually, args…​ is empty. Non-empty arguments are useful for stateful fancy pointers, such as transformer iterators.

reinterpret_cast_array<T2>

with no arguments: underlying elements are reinterpreted as type T2, element sizes (sizeof) have to be equal; with one count argument: produces a view where the underlying elements are interpreted as an array of count elements of type T2.

This function creates an independent copy of any (sub)array view:

subarray::…​ (these operations do not copy elements or allocate)

decay (same as prefix unary operator+)

creates a concrete independent array with the same dimension and elements as the view. Usually used to force a value type (and forcing a copy of the elements) and avoid the propagation of a reference type in combination with auto (e.g., auto A2_copy = + A[2];).

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 the contiguous pre-existing memory buffer are represented by 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.

Member types

same as for subarray

Member functions same as for subarray plus …​

(constructors)

array_ref::array_ref({e1, e2, …​}, p) constructs a D-dimensional view of the contiguous range starting at p and ending at least after the size of the multidimensional array (product of sizes). The default constructor and copy constructor are not exposed. Destructor is trivial since elements are not owned or managed.

elements

a flatten random-access and contiguous view of all the elements in the array in canonical order. (This is overwritten from the base class, to provide contiguous access)

Element access

same as for subarray

Structure access

same as for subarray

Iterators

same as for subarray

Capacity

same as for subarray

Creating views

same as for subarray

Creating arrays

same as for subarray

Relational functions

same as for subarray

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 (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. If T is copyable/default-constructible then multi::dynamic_array<T, D> is also copyable/default-constructible.

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 linkL:http://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#gslowner-ownership-pointers[gsl::dyn_array].

Member types

same as for array_ref

Member functions same as for array_ref plus …​

(constructors)

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.

(destructor)

Destructor deallocates memory and destroy the elements

operator=

assigns the elements from the source, sizes must match.

elements

A flatten random-access and contiguous view of all the elements in the array in canonical order. (This is overwritten from the base class, to provide contiguous access)

Element access

same as for array_ref

Structure access

same as for array_ref

Iterators

same as for array_ref

Capacity

same as for array_ref

Creating views

same as for array_ref

Creating arrays

same as for array_ref

Relational functions

same as for array_ref

Arrays

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>.

Member types

same as for dynamic_array (see above)

Member functions

(constructors)

array::array({e1, e2, …​}, T val = {}, Alloc = {}) constructs a D-dimensional array by allocating elements;`array::array(It first, It last)` and array::array(Range const& rng), same for a range of subarrays. dynamic_array::dynamic_array(std::initializer_list<…​>, Alloc = {}) constructs the array with elements initialized from a nested list.

(destructor)

Destructor deallocates memory and destroy the elements

operator=

assigns for a source subarray, or from another array. `array`s can be moved

operator=

assigns the elements from the source; the sizes don’t need to must match, in which case allocates

swap (friend)

swaps the elements from the source O(1); the sizes don’t need to match

Element access

same as for dynamic_array

Structure access

same as for dynamic_array

Iterators

same as for dynamic_array

Capacity

same as for dynamic_array

Creating views

same as for dynamic_array

Creating arrays

same as for dynamic_array

Relational functions

same as for dynamic_array

Manipulation

clear

Erases all elements from the container. The array is resized to zero size.

reextent

Changes the size of the array to new extensions. reextent({e1, e2, …​}) 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 extensions_type, and the second is optional for element types with a default constructor.

Restriction

A restriction is an array to generates its elements lazily on the fly. It is defined by combining a D-dimensional function with an D-dimensinal extensions. The function is therefore now 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 is not since there is no actual memory as its elements are not stored.

All member functions are constexpr unless specified otherwise.

multi::restriction<D, F> member types

restriction::…​ Description

(constructor)

takes 2 arguments, a extensions type of dimension D and a function that takes D arguments

value_type

multi::restriction<D - 1, FrontBindedF> or, for D == 1, the return type of F (decayed into a value if necessary, the element type)

reference

multi::restriction<D - 1, FrontBindedF> or, for D == 1, the return type of F

const_reference

Same as reference in general

index

indexing type in the leading dimension (usually std::ptrdiff_t)

size_type

describe size (number of subarrays) in the leading dimension (signed version of pointer size type, usually std::diffptr_t)

index_range

describe ranges of indices, constructible from braced indices types or from an extension_type. Can be continuous (e.g. {2, 14}) or strided (e.g. {2, 14, /every/ 3})

extension_type

describe a contiguous range of indices, constructible from braced index (e.g. {0, 10}) or from a single integer size (e.g. 10, equivalent to {0, 10}).

difference_type

describe index differences in leading dimension (signed version of pointer size type, usually std::ptrdiff_t)

operator[]

access with an index value in the leading dimension (by binding the underlying function in the first argument), it returns an object of type reference.

iterator

describe a random-access iterator in the leading dimension (immutable)

const_iterator

describe a random-access iterator in the leading dimension (immutable)

elements

returns a flat range that generates the elements in the canonical order, take no argument.

begin

returns an iterator to the start of the sequence in the leading dimension

end

returns an iterator to the start of the sequence in the leading dimension

home

returns an indexable cursor

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 invokable" concept (std::regular_invokable<F, Args…​> is true and sizeof…​(Args) is the number of dimensions D.)

Iterators

The library offers random-access iterator 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 created 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).

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) are regular (i.e. different element 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 simply 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 tranposes the array
		.template static_array_cast<Complex, conjr<Complex>>(conj)  // lazy conjugate elements
	;
}

int main() {
	using namespace std::complex_literals;
	multi::array 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 );

Since element_transformed is a reference-like object (transformed view) to the original data, it is important to understand the semantics of evaluation and possible allocations incurred. As mentioned in other sections using auto and/or + appropriately can lead to simple and efficient expressions.

Construction Allocation of `T`s Initialization (of `T`s) Evaluation (of fun) Notes

multi::array<T, D> const B = A.element_transformed(fun);

Yes

No

Yes

Implicit conversion to T if result is different, dimensions must match. B can be mutable.

multi::array<T, D> const B = + A.element_transformed(fun);

Yes (and move, or might allocate twice if types don’t match)

No

Yes

Not recommended

multi::array<T, D> const B{A.element_transformed(fun)};

Yes

No

Yes

Explicit conversion to T if result is different, dimensions must match

auto const B = + A.element_transformed(fun);

Yes

No

Yes

Types and dimension are deduced, result is contiguous, preferred

auto const B = A.element_transformed(fun);

No

No

No (delayed)

Result is effective a reference, may dangle with A, usually const, not recommended

auto const& B = A.element_transformed(fun);

No

No

No (delayed)

Result is effective a reference, may dangle with A. Preferred way.

multi::array<T, D> B(A.extensions()); B = A.element_transformed(fun);

Yes

Yes (during construction)

Yes

"Two-step" construction. B is mutable. Not recommended

Assignment Allocation of `T`s Initialization (of `T`s) Evaluation (of fun) Notes

B = A.element_transformed(fun);

No, if sizes match

Possibly (when B was initialized)

Yes

B can’t be declared const, it can be a writable subarray, preferred

B = + A.element_transformed(fun);

Yes

Possibly (when B was initialized)

Yes

Not recommended.