Array declaration and initialization in C++11

10.1k views Asked by At

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

4

There are 4 answers

2
dyp On BEST ANSWER

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.

0
AudioBubble On

This answer links a bug report, in which -Wmissing-braces is no longer enabled by default when using -Wall. If you turn on -Wmissing-braces, gcc will complain about 0, 2, 4 and 6 (same as clang).

Brace elision is allowed to occur for statements in the form T a = { ... } but not T a { }.

Why is the C++ initializer_list behavior for std::vector and std::array different?

Here is James McNellis's answer:

However, these extra braces may only be elided "in a declaration of the form T x = { a };" (C++11 ยง8.5.1/11), that is, when the old style = is used . This rule allowing brace elision does not apply for direct list initialization. A footnote here reads: "Braces cannot be elided in other uses of list-initialization."

There is a defect report concerning this restriction: CWG defect #1270. If the proposed resolution is adopted, brace elision will be allowed for other forms of list initialization, ...

If the proposed resolution is adopted, brace elision will be allowed for other forms of list initialization, and the following will be well-formed: std::array y{ 1, 2, 3, 4 };

And Xeo's answer:

... while std::array has no constructors and the {1, 2, 3, 4} braced init-list is in fact not interpreted as a std::initializer_list, but aggregate initialization for the inner C-style array of std::array (that's where the second set of braces comes from: One for std::array, one for the inner C-style member array).

std::array does not have a constructor that takes an initializer_list. For this reason, it is treated as aggregate initialization. If it did, it would look something like this:

#include <array>
#include <initializer_list>

struct test {
    int inner[3];

    test(std::initializer_list<int> list) {
        std::copy(list.begin(), list.end(), inner);
    }
};

#include <iostream>

int main() { 
    test t{1, 2, 3};
    test t2({1, 2, 3});
    test t3 = {1, 2, 3};
    test t4 = test({1, 2, 3});
    for (int i = 0; i < 3; i++)
        std::cout << t.inner[i];
    for (int i = 0; i < 3; i++)
        std::cout << t2.inner[i];
    for (int i = 0; i < 3; i++)
        std::cout << t3.inner[i];
    for (int i = 0; i < 3; i++)
        std::cout << t4.inner[i];
}
3
Sebastian Redl On

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}};
0
rems4e On

The last 2 are redundant : you could use the 6 forms of array declaration on the right side of the assignment. In addition, if your compiler doesn't optimize out the copy, these versions are less efficient.

The double braces are required with initializer-list constructor, so your third line is invalid.