C++11 Initializer List Fails - But Only on Lists of Length 2

C++11 initializer list fails - but only on lists of length 2

Introduction

Imagine the following declaration, and usage:

struct A {
A (std::initializer_list<std::string>);
};

A {{"a"          }}; // (A), initialization of 1 string
A {{"a", "b" }}; // (B), initialization of 1 string << !!
A {{"a", "b", "c"}}; // (C), initialization of 3 strings

In (A) and (C), each c-style string is causing the initialization of one (1) std::string, but, as you have stated in your question, (B) differs.

The compiler sees that it's possible to construct a std::string using a begin- and end-iterator, and upon parsing statement (B) it will prefer such construct over using "a" and "b" as individual initializers for two elements.

A { std::string { "a", "b" } }; // the compiler's interpretation of (B)


Note: The type of "a" and "b" is char const[2], a type which can implicitly decay into a char const*, a pointer-type which is suitable to act like an iterator denoting either begin or end when creating a std::string.. but we must be careful: we are causing undefined-behavior since there is no (guaranteed) relation between the two pointers upon invoking said constructor.



Explanation

When you invoke a constructor taking an std::initializer_list using double braces {{ a, b, ... }}, there are two possible interpretations:

  1. The outer braces refer to the constructor itself, the inner braces denotes the elements to take part in the std::initializer_list, or:

  2. The outer braces refer to the std::initializer_list, whereas the inner braces denotes the initialization of an element inside it.

It's prefered to do 2) whenever that is possible, and since std::string has a constructor taking two iterators, it is the one being called when you have std::vector<std::string> {{ "hello", "there" }}.

Further example:

std::vector<std::string> {{"this", "is"}, {"stackoverflow"}}.size (); // yields 2


Solution

Don't use double braces for such initialization.

C++11 initializer list length is not checked in implicit constructor

N3337 8.5.1/7

If there are fewer initializer-clauses in the list than there are members in the aggregate, then each member
not explicitly initialized shall be initialized from an empty initializer list (8.5.4).

struct S { int a; const char* b; int c; };
S ss = { 1, "asdf" };

initializes ss.a with 1, ss.b with "asdf", and ss.c with the value of an expression of the form int(),
that is, 0
.

So in your example first 5 elements are initialized with rand() other with int() which is 0.

Why does C++11 not support designated initializer lists as C99?

C++ has constructors. If it makes sense to initialize just one member then that can be expressed in the program by implementing an appropriate constructor. This is the sort of abstraction C++ promotes.

On the other hand the designated initializers feature is more about exposing and making members easy to access directly in client code. This leads to things like having a person of age 18 (years?) but with height and weight of zero.


In other words, designated initializers support a programming style where internals are exposed, and the client is given flexibility to decide how they want to use the type.

C++ is more interested in putting the flexibility on the side of the designer of a type instead, so designers can make it easy to use a type correctly and difficult to use incorrectly. Putting the designer in control of how a type can be initialized is part of this: the designer determines constructors, in-class initializers, etc.

Modeling Initializer Lists in C++03

It seems clunky and not very user-friendly. Is it possible to reduce this to a single line in C++03?

Since you are basically imitating std::array, just rid of the constructor:

template <typename T, int N>
class N_Tuple
{
public:
T values_[N];
};

Note, in this, you have to make the values_ public.

That way you can initialize them just as you expect:

N_Tuple<int,3> p = {1,2,3};

This is valid in both C++03 and C++11, no extensions of any kind (Tr1, Boost or whatever) required.

Another alternative is to hide the helper array behind a macro. The basic idea is to initialize your helper array and from there your tuple-esque array. But since you'd want the array's contents to come after in the initializing notation (var x = contents) you'd have to use something like a prolog-epilog macro which will require some repetition and you'll have to be careful with the comma in your particular type's case:

SOME_MACRO_BEGIN(My_Sequence_type, t) = {1, 2, 3} 
SOME_MACRO_END(My_Sequence_type, t);

I've worked on such a solution that is compatible with both C++03 and C++11 (provided you do implement an initializer_list constructor for the C++11 case) without any particular requirements on the C++03 side of things.

(I am not sure how would it ever be possible to do it in a single macro, since a list of elements would contain commas which are processed specially for the macro and variadic macros are not a thing in C++03)

But unless your list is short or you abbreviate the names for the prolog-epilog macros a lot, it won't likely fit in a single line.

(Moreover, it still requires copying of the data, even though for the most simple kinds of types that won't ever matter)

Why can an initializer list only be used on declaration?

Arrays are second-class citizens in C++.
They are objects,
but they are severely restricted:
they can't be copied,
they are decayed into pointers in various contexts, etc.
Consider using std::array,
which is a (fixed-size) wrapper on top of builtin arrays,
but is a first-class citizen which supports various convenience features:

std::array<int, 3> my_array = {10, 20, 30};
my_array = {40, 50, 60};

This works because, per [array.overview]/2,

std::array is an aggregate type that can be list-initialized with up
to N elements whose types are convertible to T.

live demo

This also works with std::vector.
Vectors are a different story,
so I am not going to go into details here.


If you prefer to insist on builtin arrays,
here's a workaround I designed to enable
assigning a list of values to a builtin array
(respecting value categories),
using template metaprogramming techniques.
A compile-time error is (correctly) raised
if the length of the array and the value list mismatch.
(Thanks to Caleth's
comment for pointing this out!)
Note that copying builtin arrays is impossible in C++;
that's why we have to pass the array to the function.

namespace detail {
template <typename T, std::size_t N, std::size_t... Ints, typename... Args>
void assign_helper(T (&arr)[N], std::index_sequence<Ints...>, Args&&... args)
{
((arr[Ints] = args), ...);
}
}

template <typename T, std::size_t N, typename... Args>
void assign(T (&arr)[N], Args&&... args)
{
return detail::assign_helper(arr, std::make_index_sequence<N>{}, std::forward<Args>(args)...);
}

And to use it:

int arr[3] = {10, 20, 30};
assign(arr, 40, 50, 60);

Now arr consists of 40, 50, 60.

live demo

Which greedy initializer-list examples are lurking in the Standard Library?

I assume, with your examples for std::vector<int> and std::string you meant to also cover the other containers, e.g., std::list<int>, std::deque<int>, etc. which have the same problem, obviously, as std::vector<int>. Likewise, the int isn't the only type as it also applies to char, short, long and their unsigned version (possibly a few other integral types, too).

I think there is also std::valarray<T> but I'm not sure if T is allowed to be integral type. Actually, I think these have different semantics:

std::valarray<double>(0.0, 3);
std::valarray<double>{0.0, 3};

There are a few other standard C++ class templates which take an std::initializer_list<T> as argument but I don't think any of these has an overloaded constructor which would be used when using parenthesis instead of braces.

Using double braces e.g. Foo{{initializer list}} to resolve ambiguity

In the end I solved this problem by having my class use a variadic parameter pack rather than an initializer list.

I haven't yet come across an example where you can't bypass initializer lists in this way.



Related Topics



Leave a reply



Submit