C++11: Correct Std::Array Initialization

C++11: Correct std::array initialization?

This is the bare implementation of std::array:

template<typename T, std::size_t N>
struct array {
T __array_impl[N];
};

It's an aggregate struct whose only data member is a traditional array, such that the inner {} is used to initialize the inner array.

Brace elision is allowed in certain cases with aggregate initialization (but usually not recommended) and so only one brace can be used in this case. See here: C++ vector of arrays

Array declaration and initialization in C++11

C++11 summary / TL;DR

  • Due to the brace elision defect, examples 0, 2, 6 are not required to work. Recent version of compilers however implement the proposed resolution for that defect, so that these examples will work.
  • As it is unspecified whether std::array contains a raw array. Therefore, examples 1, 3, 5, 7 are not required to work. However, I do not know of a Standard Library implementation where they do not work (in practice).
  • Example 4 will always work: std::array<int, 3> arr4 = {1, 2, 3};

I'd prefer version 4 or version 2 (with the brace elision fix), since they initialize directly and are required/likely to work.

For Sutter's AAA style, you can use auto arrAAA = std::array<int, 3>{1, 2, 3};, but this requires the brace elision fix.


std::array is required to be an aggregate [array.overview]/2, this implies it has no user-provided constructors (i.e. only default, copy, move ctor).


std::array<int, 3> arr0({1, 2, 3});
std::array<int, 3> arr1({{1, 2, 3}});

An initialization with (..) is direct-initialization. This requires a constructor call. In the case of arr0 and arr1, only the copy/move constructor are viable. Therefore, those two examples mean create a temporary std::array from the braced-init-list, and copy/move it to the destination. Through copy/move elision, the compiler is allowed to elide that copy/move operation, even if it has side effects.

N.B. even though the temporaries are prvalues, it might invoke a copy (semantically, before copy elision) as the move ctor of std::array might not be implicitly declared, e.g. if it were deleted.


std::array<int, 3> arr6 = std::array<int, 3>({1, 2, 3});
std::array<int, 3> arr7 = std::array<int, 3>({{1, 2, 3}});

These are examples of copy-initialization. There are two temporaries created:

  • through the braced-init-list {1, 2, 3} to call the copy/move constructor
  • through the expression std::array<int, 3>(..)

the latter temporary then is copied/moved to the named destination variable. The creation of both temporaries can be elided.

As far as I know, an implementation could write an explicit array(array const&) = default; constructor and not violate the Standard; this would make those examples ill-formed. (This possibility is ruled out by [container.requirements.general], kudos to David Krauss, see this discussion.)


std::array<int, 3> arr2{1, 2, 3};
std::array<int, 3> arr3{{1, 2, 3}};
std::array<int, 3> arr4 = {1, 2, 3};
std::array<int, 3> arr5 = {{1, 2, 3}};

This is aggregate-initialization. They all "directly" initialize the std::array, without calling a constructor of std::array and without (semantically) creating a temporary array. The members of the std::array are initialized via copy-initialization (see below).


On the topic of brace-elision:

In the C++11 Standard, brace elision only applies to declarations of the form T x = { a }; but not to T x { a };. This is considered a defect and will be fixed in C++1y, however the proposed resolution is not part of the Standard (DRWP status, see top of the linked page) and therefore you cannot count on your compiler implementing it also for T x { a };.

Therefore, std::array<int, 3> arr2{1, 2, 3}; (examples 0, 2, 6) are ill-formed, strictly speaking. As far as I know, recent versions of clang++ and g++ allow the brace elision in T x { a }; already.

In example 6, std::array<int, 3>({1, 2, 3}) uses copy-initialization: the initialization for argument passing is also copy-init. The defective restriction of brace elision however, "In a declaration of the form T x = { a };", also disallows brace elision for argument passing, since it's not a declaration and certainly not of that form.


On the topic of aggregate-initialization:

As Johannes Schaub points out in a comment, it is only guaranteed that you can initialize a std::array with the following syntax [array.overview]/2:

array<T, N> a = { initializer-list };

You can deduce from that, if brace-elision is allowed in the form T x { a };, that the syntax

array<T, N> a { initializer-list };

is well-formed and has the same meaning. However, it is not guaranteed that std::array actually contains a raw array as its only data member (also see LWG 2310). I think one example could be a partial specialization std::array<T, 2>, where there are two data members T m0 and T m1. Therefore, one cannot conclude that

array<T, N> a {{ initializer-list }};

is well-formed. This unfortunately leads to the situation that there's no guaranteed way of initializing a std::array temporary w/o brace elision for T x { a };, and also means that the odd examples (1, 3, 5, 7) are not required to work.


All of these ways to initialize a std::array eventually lead to aggregate-initialization. It is defined as copy-initialization of the aggregate members. However, copy-initialization using a braced-init-list can still directly initialize an aggregate member. For example:

struct foo { foo(int); foo(foo const&)=delete; };
std::array<foo, 2> arr0 = {1, 2}; // error: deleted copy-ctor
std::array<foo, 2> arr1 = {{1}, {2}}; // error/ill-formed, cannot initialize a
// possible member array from {1}
// (and too many initializers)
std::array<foo, 2> arr2 = {{{1}, {2}}}; // not guaranteed to work

The first tries to initialize the array elements from the initializer-clauses 1 and 2, respectively. This copy-initialization is equivalent to foo arr0_0 = 1; which in turn is equivalent to foo arr0_0 = foo(1); which is illegal (deleted copy-ctor).

The second does not contain a list of expressions, but a list of initializers, therefore it doesn't fulfil the requirements of [array.overview]/2. In practice, std::array contains a raw array data member, which would be initialized (only) from the first initializer-clause {1}, the second clause {2} then is illegal.

The third has the opposite problem as the second: It works if there is an array data member, but that isn't guaranteed.

Default initialization of std::array?

By definition, default initialization is the initialization that occurs when no other initialization is specified; the C++ language guarantees you that any object for which you do not provide an explicit initializer will be default initialized (C++11 §8.5/11). That includes objects of type std::array<T, N> and T[N].

Be aware that there are types for which default initialization has no effect and leaves the object's value indeterminate: any non-class, non-array type (§8.5/6). Consequently, a default-initialized array of objects with such types will have indeterminate value, e.g.:

int plain_int;
int c_style_array[13];
std::array<int, 13> cxx_style_array;

Both the c-style array and std::array are filled with integers of indeterminate value, just as plain_int has indeterminate value.

Is there a syntax that will work on all arrays (including zero-sized arrays) to initialize all elements to their default value?

I'm guessing that when you say "to their default value" you really mean "initialize all elements to T{}". That's not default-initialization, it is value-initialization (8.5/7). You can request value initialization quite easily in C++11 by giving each declaration an empty initializer:

int plain_int{};
int c_style_array[13]{};
std::array<int, 13> cxx_style_array{};

Which will value-initialize all of the array elements in turn, resulting in plain_int, and all the members of both kinds of arrays, being initialized to zero.

Initializing a std::array with a constant value

With std::index_sequence, you might do:

namespace detail
{
template <typename T, std::size_t ... Is>
constexpr std::array<T, sizeof...(Is)>
create_array(T value, std::index_sequence<Is...>)
{
// cast Is to void to remove the warning: unused value
return {{(static_cast<void>(Is), value)...}};
}
}

template <std::size_t N, typename T>
constexpr std::array<T, N> create_array(const T& value)
{
return detail::create_array(value, std::make_index_sequence<N>());
}

With usage

auto a = create_array<10 /*, int*/>(7); // auto is std::array<int, 10>

Which, contrary to std::fill solution, handle non default constructible types.

std::array c++11 initializer syntax error

Instead of the one pair of braces you need two.

myarray = {{1,2,3,4,5}};

Initialisation of std::array

Short version: An initializer-clause that starts with { stops brace-elision. This is the case in the first example with {1,2}, but not in the third nor fourth which use A{1,2}. Brace-elision consumes the next N initializer-clauses (where N is dependent on the aggregate to be initialized), which is why only the first initializer-clause of the N must not begin with {.


In all implementations of the C++ Standard Library I know of, std::array is a struct which contains a C-style array. That is, you have an aggregate which contains a sub-aggregate, much like

template<typename T, std::size_t N>
struct array
{
T __arr[N]; // don't access this directly!
};

When initializing a std::array from a braced-init-list, you'll therefore have to initialize the members of the contained array. Therefore, on those implementations, the explicit form is:

std::array<A, 4> x = {{ {1,2}, {3,4}, {5,6}, {7,8} }};

The outermost set of braces refers to the std::array struct; the second set of braces refers to the nested C-style array.


C++ allows in aggregate initialization to omit certain braces when initializing nested aggregates. For example:

struct outer {
struct inner {
int i;
};
inner x;
};

outer e = { { 42 } }; // explicit braces
outer o = { 42 }; // with brace-elision

The rules are as follows (using a post-N4527 draft, which is post-C++14, but C++11 contained a defect related to this anyway):

Braces can be elided in an initializer-list as follows. If the
initializer-list begins with a left brace, then the succeeding
comma-separated list of initializer-clauses initializes the members of
a subaggregate; it is erroneous for there to be more
initializer-clauses than members. If, however, the initializer-list
for a subaggregate does not begin with a left brace, then only
enough initializer-clauses from the list are taken to initialize the
members of the subaggregate; any remaining initializer-clauses are
left to initialize the next member of the aggregate of which the
current subaggregate is a member.

Applying this to the first std::array-example:

static std::array<A, 4> x1 =
{
{ 1, 2 },
{ 3, 4 },
{ 5, 6 },
{ 7, 8 }
};

This is interpreted as follows:

static std::array<A, 4> x1 =
{ // x1 {
{ // __arr {
1, // __arr[0]
2 // __arr[1]
// __arr[2] = {}
// __arr[3] = {}
} // }

{3,4}, // ??
{5,6}, // ??
...
}; // }

The first { is taken as the initializer of the std::array struct. The initializer-clauses {1,2}, {3,4} etc. then are taken as the initializers of the subaggregates of std::array. Note that std::array only has a single subaggregate __arr. Since the first initializer-clause {1,2} begins with a {, the brace-elision exception does not occur, and the compiler tries to initialize the nested A __arr[4] array with {1,2}. The remaining initializer-clauses {3,4}, {5,6} etc. do not refer to any subaggregate of std::array and are therefore illegal.

In the third and fourth example, the first initializer-clause for the subaggregate of std::array does not begin with a {, therefore the brace elision exception is applied:

static std::array<A, 4> x4 =
{
A{ 1, 2 }, // does not begin with {
{ 3, 4 },
{ 5, 6 },
{ 7, 8 }
};

So it is interpreted as follows:

static std::array<A, 4> x4 =
{ // x4 {
// __arr { -- brace elided
A{ 1, 2 }, // __arr[0]
{ 3, 4 }, // __arr[1]
{ 5, 6 }, // __arr[2]
{ 7, 8 } // __arr[3]
// } -- brace elided
}; // }

Hence, the A{1,2} causes all four initializer-clauses to be consumed to initialize the nested C-style array. If you add another initializer:

static std::array<A, 4> x4 =
{
A{ 1, 2 }, // does not begin with {
{ 3, 4 },
{ 5, 6 },
{ 7, 8 },
X
};

then this X would be used to initialize the next subaggregate of std::array. E.g.

struct outer {
struct inner {
int a;
int b;
};

inner i;
int c;
};

outer o =
{ // o {
// i {
1, // a
2, // b
// }
3 // c
}; // }

Brace-elision consumes the next N initializer-clauses, where N is defined via the number of initializers required for the (sub)aggregate to be initialized. Therefore, it only matters whether or not the first of those N initializer-clauses starts with a {.

More similarly to the OP:

struct inner {
int a;
int b;
};

struct outer {
struct middle {
inner i;
};

middle m;
int c;
};

outer o =
{ // o {
// m {
inner{1,2}, // i
// }
3 // c
}; // }

Note that brace-elision applies recursively; we can even write the confusing

outer o =
{ // o {
// m {
// i {
1, // a
2, // b
// }
// }
3 // c
}; // }

Where we omit both the braces for o.m and o.m.i. The first two initializer-clauses are consumed to initialize o.m.i, the remaining one initializes o.c. Once we insert a pair of braces around 1,2, it is interpreted as the pair of braces corresponding to o.m:

outer o =
{ // o {
{ // m {
// i {
1, // a
2, // b
// }
} // }
3 // c
}; // }

Here, the initializer for o.m does start with a {, hence brace-elision does not apply. The initializer for o.m.i is 1, which does not start with a {, hence brace-elision is applied for o.m.i and the two initializers 1 and 2 are consumed.

c++11 array initialization won't call copy constructor

You've encountered a defect in C++: list-initialization from a single element. The behaviour specified in the C++11 and C++14 International Standard is surprising. I'll refer to C++14 below.

Template instantiations of std::array are aggregates [array.overview]/2. Therefore, when initializing std::array objects from a braced-init-list, aggregate-initialization will be performed indiscriminately of the number of initializers [dcl.init.list]/3.1. Other container classes cannot be aggregates because of the requirements for certain constructions (e.g. from a pair of iterators).

Aggregate-initialization initializes (potentially recursively) the data members from the initializers. In your case, it will try to initialize the first data member of std::array<sf::Keyboard::Key, N> from the initializer sequence (which is of the same type). For all implementations of std::array I know, the first data member of std::array is a C-style array. List-initialization will then try to initialize the first element of that array from the original initializer: sequence.

Example:

struct aggregate
{
int m[2];
};

aggregate x = {0, 1};
assert(x.m[0] == 0 && x.m[1] == 1);

aggregate y{x}; // error: cannot convert `aggregate` to `int`

The initialization in the last line will try to initialize y.m[0] from x.


CWG issue 1467 describes this and a related issue, list-initializing when there are no initializers. The proposed resolution introduces a (yet another) special case for list-initialization that covers the issue in the OP. Quoting a recent github draft, [dcl.init.list]/3.1

If T is a class type and the initializer list has a single element of
type cv U, where U is T or a class derived from T, the object is
initialized from that element (by copy-initialization for
copy-list-initialization, or by direct-initialization for
direct-list-initialization).

Aggregate-initialization in recent drafts has lower "priority" (3.3), that is, will only be performed if the condition above is not met.


Recent versions of g++ (5.0) and clang++ (3.7.0) implement the proposed resolution even in C++11 mode.

Initialization of std::array with std::initializer_list in constructor's initialization list

std::array was designed (in the Boost library) to support the braces initialization syntax with C++03. The only way to do that in C++03 was as a POD (plain old data) type, one with no constructors. Initializer lists were introduced in C++11, along with std::array, but std::array was not changed from its Boost version to use initializer lists. So, it's historical.

By the way, note that the reinterpret_cast is dangerous here because the initializer list may contain fewer items than the array.



Related Topics



Leave a reply



Submit