Why Is the C++ Initializer_List Behavior For Std::Vector and Std::Array Different

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

std::array<T, N> is an aggregate: it doesn't have any user-declared constructors, not even one taking a std::initializer_list. Initialization using braces is performed using aggregate initialization, a feature of C++ that was inherited from C.

The "old style" of aggregate initialization uses the =:

std::array<int, 4> y = { { 1, 2, 3, 4 } };

With this old style of aggregate initialization, extra braces may be elided, so this is equivalent to:

std::array<int, 4> y = { 1, 2, 3, 4 };

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, and the following will be well-formed:

std::array<int, 4> y{ 1, 2, 3, 4 };

(Hat tip to Ville Voutilainen for finding the defect report.)

Why did STL made a(nother) distinction between a std::array and a std::initializer_list

The question betrays some misunderstandings about what is going on here.

std::array is an array object. It is an object whose storage size is the storage for its array elements. std::initializer_list is a pointer to an array (one created by the compiler). You can heap-allocate std::array for example; you cannot heap-allocate std::initializer_list. Well, you can, but you'd just be heap allocating a pointer, which usually isn't helpful. This:

auto *p = new std::initializer_list<int>{1, 2, 3, 4};

Is broken code, as it heap allocates a pointer to an array temporary created by the compiler. An array temporary that is immediately destroyed at the end of this statement. So you now have a pointer to an array that does not exist, so using *p or p[x] is undefined behavior.

Doing the same with std::array works fine, because array is an array.

Furthermore, the purpose of std::initializer_list is not to "define a list at compile time". As the name suggests, the point of the type is to create a list for the purpose of initializing an object. This is why the ultimate source of its data cannot be provided by any means other than a specific piece of C++ grammar, one used specifically to initialize objects: a braced-init-list.

You can use std::initializer_list as a quick-and-dirty way of just creating an array of values for some purpose. But that's not the point of the type. That's why it doesn't have an operator[]; because functions that can be initialized from a sequence of values don't tend to need that particular operation. They usually only need to walk it from beginning to end.

How come std::initializer_list is allowed to not specify size AND be stack allocated at the same time?

The thing is, std::initializer_list does not hold the objects inside itself. When you instantiate it, compiler injects some additional code to create a temporary array on the stack and stores pointers to that array inside the initializer_list. For what its worth, an initializer_list is nothing but a struct with two pointers (or a pointer and a size):

template <class T>
class initializer_list {
private:
T* begin_;
T* end_;
public:
size_t size() const { return end_ - begin_; }
T const* begin() const { return begin_; }
T const* end() const { return end_; }

// ...
};

When you do:

foo({2, 3, 4, 5, 6});

Conceptually, here is what is happening:

int __tmp_arr[5] {2, 3, 4, 5, 6};
foo(std::initializer_list{arr, arr + 5});

One minor difference being, the life-time of the array does not exceed that of the initializer_list.

Initializing std::pair via std::initializer_list doesn't work while initializing an std::array?

Initializing std::array with a braced initializer list is a bit tricky, because you need an extra set of braces (as it is an aggregate):

array<pair<char,char>, 3> p = {{ {'{','}'}, {'[',']'}, {'(',')'} }};
^ ^

std::vector is different, because using a braced initializer list will result in the std::initializer_list constructor being called, and not using aggregate initialization like std::array.

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.

Why can't a 2D std::array be initialized with two layers of list-initializers?

The container std::array is equivalently a struct holding a C-array (an implementation may not implement std::array in this way, but it should guarantee the semantic is the same), so it should be initialized by two layers of braces, i.e.

#include <array>
std::array<std::array<double,2>,2> f() {
return {{{{0,0}},{{0,0}}}};
}

Of course, braces in an initializer-list can be elided like what we usually do for a 2D array:

int arr[2][2] = {0,1,2,3};

... but the initializer-list that begins with the elided braces before the elision should not begin with a left brace after the elision. In other words, if an initializer-list begins with a left brace, the compiler will not consider the possibility that this initializer-list has elided outermost braces.

In your initializer {{0,0},{0,0}}, the sub-initializer {0,0},{0,0} begins with a left brace, so it is used to initialize the C-array itself. However, there are two clauses in the list while there is only one C-array, an error occurs.

In your initializer {std::array<double,2>{0,0},{0,0}}, the sub-initializer std::array<double,2>{0,0},{0,0} does not begin with a left brace, so it can be used to initialize the elements of the C-array, which is OK (recursively, {0,0} is OK to initialize an std::array<double,2> because the sub-initializer 0,0 does not begin with a left brace).


A suggestion: with this elision rule of braces, you can elide all inner braces, just like what we usually do for a 2D array:

#include <array>
std::array<std::array<double,2>,2> f() {
return {0,0,0,0};
}

Can I initialize an array using the std::initializer_list instead of brace-enclosed initializer?

Other answered correctly said this is not possible upfront. But with little helpers, you can get pretty close

template<typename T, std::size_T N, std::size_t ...Ns>
std::array<T, N> make_array_impl(
std::initializer_list<T> t,
std::index_sequence<Ns...>)
{
return std::array<T, N>{ *(t.begin() + Ns) ... };
}

template<typename T, std::size_t N>
std::array<T, N> make_array(std::initializer_list<T> t) {
if(N > t.size())
throw std::out_of_range("that's crazy!");
return make_array_impl<T, N>(t, std::make_index_sequence<N>());
}

If you are open to more work arounds, you can put this into a class to catch statically-known length violations for the cases where you pass a braced init list. But be warned that most people who read this code will head-desk

template<typename T, std::size_t N>
struct ArrayInitializer {
template<typename U> struct id { using type = U; };
std::array<T, N> t;

template<typename U = std::initializer_list<T>>
ArrayInitializer(typename id<U>::type z)
:ArrayInitializer(z, std::make_index_sequence<N>())
{
if(N > z.size())
throw std::out_of_range("that's crazy!");
}

template<typename ...U>
ArrayInitializer(U &&... u)
:t{ std::forward<U>(u)... }
{ }

private:
template<std::size_t ...Ns>
ArrayInitializer(std::initializer_list<T>& t,
std::index_sequence<Ns...>)
:t{ *(t.begin() + Ns) ... }
{ }
};

template<typename T, std::size_t N>
std::array<T, N> f(ArrayInitializer<T, N> ai) {
return std::move(ai.t);
}

int main() {
f<int, 5>({1, 2, 3, 4, 5}); // OK
f<int, 5>({1, 2, 3, 4, 5, 6}); // "too many initializers for array<int, 5>"

std::initializer_list<int> il{1, 2, 3, 4, 5};
f<int, 5>(il); // ok
}

Note that both the non-static case at the top of the answer and the "head-desk" case do only check whether you provide too few initializing elements, and errors out then, for the initializer_list case. If you provide too many for the initializer_list case, the trailing elements are just ignored.

Different behaviour in C++ std::vector construction with gcc and clang

This is CWG 2137, which only gcc implements at present.

Clang bug: https://github.com/llvm/llvm-project/issues/24186

See also C++ constructor taking an std::initializer_list of size one - this is slightly more complicated in that the initializer list element is constructible from its argument (of the type being initialized), but the diagnosis is the same, and I can't do any better than T.C.'s description:

Clang implemented DR 1467 (brace-initializing a T from a T behaves as if you didn't use braces) but has yet to implement DR 2137 (on second thought, do that only for aggregates).

If you're OK changing your program syntax slightly, you could add another level of braces:

Variant::Vector v{{Variant::Vector{10, 20}}};

Or add parentheses:

Variant::Vector v({Variant::Vector{10, 20}});

Same clang, different results for std::initializer_list program with -std=c++14/-std=c++17

This has to do with guaranteed copy elision, a new language feature in C++17.

This line (reduced):

static auto results = std::initializer_list<string>{x, y};

In C++14 constructs an initializer list and then moves it into results - which immediately dangles because initializer_list doesn't manage any lifetimes (a std::initializer_list has a backing const array with the same lifetime as the initial object - once the initial initializer_list is destroyed at the end of the line, so is the backing array).

In other words, in C++14, this program has undefined behavior.

In C++17 it behaves exactly like:

static std::initalizer_list<string> results{x, y};

In this case, the backing array has the same lifetime as results, which is for the length of the program. This program has well-defined behavior.



Related Topics



Leave a reply



Submit