Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Array declaration and initialization in C++11

Here are 8 ways to declare and initialize arrays in C++11 that seems ok under g++:

/*0*/ std::array<int, 3> arr0({1, 2, 3});
/*1*/ std::array<int, 3> arr1({{1, 2, 3}});
/*2*/ std::array<int, 3> arr2{1, 2, 3};
/*3*/ std::array<int, 3> arr3{{1, 2, 3}};
/*4*/ std::array<int, 3> arr4 = {1, 2, 3};
/*5*/ std::array<int, 3> arr5 = {{1, 2, 3}};
/*6*/ std::array<int, 3> arr6 = std::array<int, 3>({1, 2, 3});
/*7*/ std::array<int, 3> arr7 = std::array<int, 3>({{1, 2, 3}});

What are the correct ones according to the strict standard (and the upcoming C++14 standard) ? What are the most common/used and those to avoid (and for what reason) ?

like image 537
Vincent Avatar asked Dec 11 '13 13:12

Vincent


2 Answers

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.

like image 198
dyp Avatar answered Oct 09 '22 11:10

dyp


I believe they are all strictly conforming, except possibly arr2. I would go with the arr3 way, because it is concise, clear, and definitely valid. If arr2 is valid (I'm just not sure), that would be even better, actually.

Combining parens and braces (0 and 1) never sits well with me, the equals (4 and 5) is ok, but I just prefer the shorter version, and 6 and 7 are just absurdly verbose.

However, you might want to go with yet another way, following Herb Sutter's "almost always auto" style:

auto arr8 = std::array<int, 3>{{1, 2, 3}};
like image 38
Sebastian Redl Avatar answered Oct 09 '22 10:10

Sebastian Redl