Reference

Fundamental types and concepts

The library interface presents several closely related C++ types (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).

When using the library, it is simpler to start from array, and other types are rarely explicitly used, especially if using auto; however, it is convenient for documentation to present the classes in a different order since the classes subarray, array_ref, dynamic_array, and array have an is-a relationship (from left to right). For example, array_ref has all the methods available to subarray, and array has all the operations of array_ref (and more).

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

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

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

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

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 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 range. Takes no argument.

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 elements

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 elements from index a to index b (non-inclusive) {S[a], …​ S[b-1]}. Preserves the dimension.

strided

(takes one integer argument s) returns a subarray skipping s elements. 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.

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.

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.

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.

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.