Proposal to Add Multi-Dimensional Support to std::array

I. Table of Contents

II. Introduction

The std::array class template adapts array types to standard
library classes. But the adaptation is only for the outermost level; the inner
dimensions of a multidimensional array type are not considered. This proposal
is to change std::array to take multiple dimensions as variadic
template arguments.

III. Motivation and Scope

When reading up on the variadic template feature, I made a multidimensional
variant of std::array as an exercise. After making the
single-extent case compatible with std::array, I realized it is
better to propose a change in std::array instead of creating a
near-identical class template. The exercise code, at <https://github.com/CTMacUser/ArrayMD>,
could be considered as a reference implementation and test suite.

I later found out that a multidimensional array class template was one of
the examples given in the original variadic template proposal.

With constructions that can express array types and array subscript usage in
comma-separated lists, array entities may be invoked during variadic template-
and function-argument processing.

IV. Impact on the Standard

The proposal adds two class templates (and corresponding type-alias
templates), three function templates, and several members to an existing class
template. It modifies descriptions of std::array in the sequence
container requirements. And it modifies the relational operator function
templates to take mixed element types. The implementation should work with
current language features.

V. Design Decisions

I adapted std::array for multidimensional arrays by making the
inner array object a built-in multidimensional array type. Since built-in types
are used, only the first level of access via operator []() has to
be a function instead of a built-in operation. Similar multidimensional
container libraries where the array class template consists of nested
instantiations of itself have the disadvantages of possible padding (making the
elements noncontiguous) and of working in the wrong units (the largest
sub-array type instead of the user's original element type).

The implementation is mostly as straight forward as the current
std::array, so there is not much effort needed from
implementers.

VI. Technical Specifications

The changes are based off C++ Working Draft Standard N3691. The section
numbers for new sections have letters as place-holders; the final numbers, plus
moving existing section numbers to fit, are to be determined later.

In section 20.11.7.4 [meta.trans.arr], add a third and fourth row to Table
55:

Addition to: Table 55 — Array modifications

Template

Comments

template <class T, size_t I>
struct remove_some_extents;

If I is zero, then the member typedef
type shall be T. Otherwise, the member
typedef type shall be the same as applying
remove_extentI times. [Note: When
T is either a non-array type or an array type with an
extent count at most I, this class template acts like
remove_all_extents. —end note]

template <class T, size_t... N>
struct append_extents;

If N is an empty parameter pack, then the member
typedef type shall be T. Otherwise, let
X be the first entry of N,
Y be a (possibly empty) parameter pack
representing the remainder of N, and
U be an alias to typename
append_extents<T, Y...>::type. Then the member typedef
type shall alias U[X] when X
is non-zero, else it shall alias U[]. [Note:
Since an array of unknown bound is not permitted to be an array
element type (8.3.4), only the first (i.e. left-most) entry of
N may be zero. —end note]

-17- The member function at() provides
bounds-checked access to container elements. at() throws
out_of_range if n >= a.size(). For
instantiations of array with an extent count besides one, there
are modifications (23.3.2.D) to the return types and the operational
semantics of a[n] and a.at().

-1- The header <array> defines a class
template for storing fixed-size sequences of objects. An array supports
random access iterators. An instance of array<T,
N...> stores
NP elements of type
T, where P is the product of the entries from
the pack expansion N, so that size() ==
NP is an invariant. The elements of
an array are stored contiguously, meaning that if a is an
array<T, N...> then it obeys the identity
&((T*)&a)[n] ==
&((T*)&a)[0] + n for all 0 <= n
< NP.

-2- An array is an aggregate (8.5.1) that
can be initialized with the syntaxarray<T, NN0, ...,
Nsizeof...(N)-1> a = { initializer-list
};
where initializer-list is a comma-separated (and possibly
braced-partitioned) list of up to
NP elements whose types are
convertible to T, and P is the product of the
entries N0 through
Nsizeof...(N)-1 from the pack expansion
N. [Note:P is 1
when N is empty. —end note]

-3- An array satisfies all of the
requirements of a container and of a reversible container (23.2), except that
a default constructed array object is not empty and that
swap does not have constant complexity. An array
satisfies some of the requirements of a sequence container (23.2.3).
Descriptions are provided here only for operations on array that
are not described in one of these tables and for operations where there is
additional semantic information.

-4- [ Note: The member variable
elems is shown for exposition only, to emphasize that
array is a class aggregate. The name elems is not
part of array’s interface. —end note ]

No modifications for section 23.3.2.2 [array.cons]:

-1- The conditions for an aggregate (8.5.1) shall be met.
Class array relies on the implicitly-declared special member
functions (12.1, 12.4, and 12.8) to conform to the container requirements
table in 23.2. In addition to the requirements specified in the container
requirements table, the implicit move constructor and move assignment
operator for array require that T be
MoveConstructible or MoveAssignable,
respectively.

-5-Requires:TO is
DefaultConstructible. TI is explicitly convertible
to TO.

-6-Returns: an object y
such that each *(y.begin() + d) is equivalent to
static_cast<TO>(*(x.begin() + d)) for 0 <= d <
min(x.size(), y.size()) and the trailing elements of
y, if any, are value-initialized.

-2-Throws: Nothing unless one of the
element-wise swap calls throws an exception.

-3-Note: Unlike the swap function
for other containers, array::swap takes linear time, may exit
via an exception, and does not cause iterators to become associated with the
other container.

Add new section 23.3.2.B [array.apply]:

template <class F> void apply(F&& f);

template <class F> void apply(F&& f) const;

template <class F> void capply(F&& f) const;

-1-Requires:f is compatible with
function<void(cv T&, size_type, ...,
size_type)>, where cv is the cv-qualification of
*this and the number of trailing size_type
arguments is extent_count.

-2-Effects:f is called once for
each element with the following constraints:

Given element e with extent_count ordered
indices i0, ..., ik required to dereference it from
elems, the application call will be compatible with
forward<F>(f)(e, i0, ...,
ik).

The order of element traversal is implementation-defined.

-3-Throws: Nothing unless an application call
throws an exception.

Author's Note: Element traversal order should be the in-memory order
or some other order that minimizes cache misses.

Add new section 23.3.2.C [array.iter], titled "array
iteration":

-1- The beginning value of forward iteration points to
the element at the address returned by data(). The past-the-end
value corresponds to data() + value_count. Propagation of
forward iteration follows the (nested) row-wise storage order (8.3.4).

Author's Note: The above section explicitly defines the intuitive
order elements are linearly visited when the array object is
multidimensional.

-4-Requires:sizeof...(SizeType) <=
extent_count. Each entry in the pack expansion SizeType
has to have an implicit conversion to size_type. Each entry in
the pack expansion n, in order, needs to be less than the
corresponding value in extents.

-5-Returns:elems followed by
sizeof...(n) calls of the built-in subscript operator, with each
operator using an entry from the pack expansion n, in order, as
an index.

-6-Throws: Nothing unless a conversion from an
entry of the pack expansion n to a size_type value
throws an exception.

-7-Remarks: If any of the types in the pack
expansion SizeType cannot be implicitly converted to
size_type, or if sizeof...(SizeType) >
extent_count, then that function shall not participate in overload
resolution.

-13-Throws: Nothing unless: a conversion from
an entry of the pack expansion n to a size_type
value throws an exception; a particular index is equal or greater than its
corresponding entry in extents, in which
out_of_range is thrown; or i.size() !=
extent_count, in which length_error is thrown.

-14-Remarks: If any of the types in the pack
expansion SizeType cannot be implicitly converted to
size_type, or if sizeof...(SizeType) >
extent_count, then that function shall not participate in overload
resolution.

Author's Note: The contained (array) object, elems, can
be directly referenced by calling operator()(), at(),
or the initializer_list overload of operator[]() with
no indexes.

-1-array shall provide support for the
special case of N being an empty pack expansion.

-2- In the case that sizeof...(N) == 0, the
dereference and extents members are not provided.
Neither are the overloads of operator[]() that take one
size_type parameter provided. It is implementation-defined if
the overloads of operator()() and at() that do not
take an initializer_list are replaced with equivalent
non-template members that take zero parameters.

Modify section 23.3.2.8 [array.zero]

-1-array shall provide support for the
special case Nvalue_count == 0.
[Note: Such array instantiations are made by using
at least one extent and setting the first extent to zero. They have the same
size and alignment specifications as instantiations with value_count ==
1. They are always DefaultConstructible, even when
T is not. —end note]

-2- In the case that
Nvalue_count == 0, the values
ofbegin() ==andend()== unique valueare unspecified but they shall be
identical. The return value of data() is unspecified.

-3- The effect of calling front() or
back() for a zero-sized array is undefined.

-4- Member function swap() shall have a
noexcept-specification which is equivalent to
noexcept(true).

Modify section 23.3.2.9 [array.tuple]

tuple_size<array<T, N...> >::value

-1-Return type: integral constant
expression.

-2-Value:Narray<T,
N...>::value_count

tuple_element<I, array<T, N...> >::type

-3-Requires:I <
Narray<T, N...>::value_count. The program
is ill-formed if I is out of bounds.

-8-Requires:I <
Narray<T, N...>::value_count. The program
is ill-formed if I is out of bounds.

-9-Returns: A const reference to the
Ith element of a, where indexing is zero-based
and uses the forward-iteration order (23.3.2.C).

Author's Note: When sizeof...(N) == 1, get
acts with the expected semantics. When N is empty, 0
is the only valid value for I and get returns the
sole element. Otherwise, the multiple indexes needed to reference an element
are flattened to the index in "row-major" linear traversal order.