65

I'm getting up to speed with C++0x, and testing things out with g++ 4.6

I just tried the following code, thinking it would work, but it doesn't compile. I get the error:

incompatible types in assignment of ‘std::initializer_list<const int>’ to ‘const int [2]’

struct Foo
  {
    int const data[2];

    Foo(std::initializer_list<int const>& ini)
    : data(ini)
    {}
  };

Foo f = {1,3};
swestrup
  • 4,079
  • 3
  • 22
  • 33

8 Answers8

71

You can use a variadic template constructor instead of an initializer list constructor:

struct foo { 
    int x[2]; 
    template <typename... T> 
    foo(T... ts) : x{ts...} { // note the use of brace-init-list
    } 
};

int main() {
    foo f1(1,2);   // OK
    foo f2{1,2};   // Also OK
    foo f3(42);    // OK; x[1] zero-initialized
    foo f4(1,2,3); // Error: too many initializers
    foo f5(3.14);  // Error: narrowing conversion not allowed
    foo f6("foo"); // Error: no conversion from const char* to int
}

EDIT: If you can live without constness, another way would be to skip initialization and fill the array in the function body:

struct foo {
    int x[2]; // or std::array<int, 2> x;
    foo(std::initializer_list<int> il) {
       std::copy(il.begin(), il.end(), x);
       // or std::copy(il.begin(), il.end(), x.begin());
       // or x.fill(il.begin());
    }
}

This way, though, you lose the compile-time bounds checking that the former solution provides.

JohannesD
  • 13,802
  • 1
  • 38
  • 30
  • 3
    I think he wants the array to be `const`… the first solution satisfies this but not the second. – Potatoswatter Apr 05 '11 at 11:09
  • Yeah, I am rather hoping that the compiler will make some optimizations based on the constness of the data. Still, I voted up the first solution as I rather liked it. Your method two is the workaround I decided to use while posting this problem, but I'd rather not have to go that route. – swestrup Apr 05 '11 at 11:50
  • 3
    @swestrup: `const` doesn't usually inform optimization; the compiler figures such things by variable lifetime analysis. `const` is a correctness check and helps the language support read-only memory. – Potatoswatter Apr 05 '11 at 15:17
  • The variadic template constructor might conflict with other constructors so I don't think it is a general solution to the problem of the OP (although it might just do in this particular case). – gnzlbg Oct 10 '14 at 12:27
  • In your second example, the line `std::copy(x, x+2, il.begin());` should be `std::copy(il.begin(), il.end(), x);` because we want to initialize x[2] from il, don't we? – plx Jan 06 '16 at 20:30
  • @plexoos Argh, indeed. `std::copy` argument order is reversed compared to `memcpy`... – JohannesD Jan 07 '16 at 19:52
  • Im trying to get your first solution to work with a char array instead, passing a string literal..I dont see why that wouldnt be possible, but Im failing hard – Icebone1000 Sep 26 '20 at 00:34
20

As far as I can tell, using list-initialization of the function argument of the constructor (8.5.4/1) should be legal and solves many of the issues of the above. However, GCC 4.5.1 on ideone.com fails to match the constructor and rejects it.

#include <array>

struct Foo
  {
    std::array< int, 2 > const data;

    Foo(std::array<int, 2> const& ini) // parameter type specifies size = 2
    : data( ini )
    {}
  };

Foo f( {1,3} ); // list-initialize function argument per 8.5.4/1

If you really insist on initializer_list, you can use reinterpret_cast to turn the underlying array of the initializer_list into a C-style array.

Foo(std::initializer_list<int> ini) // pass without reference- or cv-qualification
: data( reinterpret_cast< std::array< int, 2 > const & >( * ini.begin() )
Potatoswatter
  • 134,909
  • 25
  • 265
  • 421
  • 4
    I wanted to give this one an up-vote and a down-vote, but they wouldn't let me. – TonyK Apr 05 '11 at 10:48
  • Oh ick. I am both appalled and impressed at the same time. I REALLY hope there's a less hackish way of doing this. – swestrup Apr 05 '11 at 10:49
  • 1
    @Tony: I don't blame you, but one has to be brave to harness the forces of evil… right? – Potatoswatter Apr 05 '11 at 10:50
  • 1
    The latter solution should AFAICS indeed work: function argument passing is a direct-initialization context; list-initialization can occur in direct-initialization; list-initializing an aggregate causes aggregate-initialization (8.5.4/3) to occur; and std::array is an aggregate by design. Seems like a bug in GCC 4.5. – JohannesD Apr 05 '11 at 11:39
  • I rather like your second solution, but alas g++ 4.6 doesn't accept it either. – swestrup Apr 05 '11 at 11:57
  • If you would only remove the hackish solution... Even without the `initialiser_list`, it promotes `std::array` over C-style arrays. – xtofl Aug 20 '13 at 19:13
  • @xtofl Done. The question is a bit weird, and parameter packs definitely not a good solution here. – Potatoswatter Aug 20 '13 at 23:58
  • @Potatoswatter `reinterpret_cast` is not allowed in a constant expression, so the constructor cannot be constexpr :( – gnzlbg Oct 09 '14 at 19:08
  • Note that using `std::array` is equivalent to declaring an inner struct containing only the array you want to list-initialize, and passing a const reference of it in the constructor. When you call the constructor later on, you can simply pass the initializer list as well. – el_technic0 Apr 21 '16 at 22:34
6

Just a small addition to great JohannesD answer.

In case of no arguments passed to foo constructor, array will be default initialized. But sometimes you want to keep underlying array uninitilized (maybe for performance reasons). You cannot add default constructor along with variadic-templated one. Workaround is additional argument to variadic-templated constructor, to distinguish it from zero-argument constructor:

template<class T, size_t rows, size_t cols>
class array2d
{
    std::array<T, rows * cols> m_Data;
    public:

    array2d() {}

    template <typename T, typename... Types>
    array2d(T t, Types... ts) : m_Data{ { t, ts... } } {}
};

So, now you can brace-initilize object, or left it uninitialized:

array2d<int, 6, 8> arr = { 0, 1, 2, 3 };  // contains 0, 1, 2, 3, 0, 0, 0, ...
array2d<int, 6, 8> arr2;                  // contains garbage

Update 31/07/2016

Three years have passed quickly and compiler implementers improved standard compliance of their products up to the level where default constructor is not considered ambiguous in the presence of variadic constructor anymore. So, in practice, we don't need additional T t argument to variadic constructor to disambiguate constructors.

Both

array2d() {}

and

array2d() = default;

will leave array uninitialized if object is being constructed without arguments. This behavior is consistent on all major compilers. Full example (rextester):

#include <array>
#include <iostream>

template<class T, size_t rows, size_t cols>
class array2d
{
  public:
    std::array<T, rows * cols> m_Data;

    array2d() = default;

    template <typename... Types>
    array2d(Types... ts) : m_Data{ { ts... } } {}
};

int main()
{
    array2d<int, 6, 8> arr_init = { 0, 1, 2, 3 };
    array2d<int, 6, 8> arr_default;


    std::cout << "Initialized: \n";
    for(const auto& a : arr_init.m_Data)
        std::cout << a << " ";
    std::cout << "\n";

    std::cout << "Default: \n";
    for(const auto& a : arr_default.m_Data)    
        std::cout << a << " ";

    std::cout << "\n";
}

Output:

Initialized: 
0 1 2 3 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 
Default: 
2 0 -519559849 32558 1 32558 0 0 -519634912 32558 -526739248 32558 1 0 2 0 6295032 0 -519531243 32558 0 0 -1716075168 32765 6295648 0 4196192 0 6295648 0 -526527271 32558 1 0 2 0 6295032 0 4196845 0 124 0 0 0 4196768 0 4196518 0 

Removing default constructor still leads to variadic constructor to be called and array being default-initialized (with all-zeros in our case).

Thanks @Alek for bumping this thread and for drawing attention to these facts, and also thanks to all people working hard on compiler development.

Community
  • 1
  • 1
Ivan Aksamentov - Drop
  • 12,860
  • 3
  • 34
  • 61
  • "You cannot add default constructor along with variadic-templated one." This is only correct if the default constructor is implicit, which is not what you suggest on your answer. http://stackoverflow.com/a/2953925/259543 – alecov Jul 31 '16 at 00:56
  • @Alek Indeed. Now it also works in practice. I updated my answer. Too bad I don't remember compilers and compiler versions that triggered ambiguity previously. Thank you for the remark. – Ivan Aksamentov - Drop Jul 31 '16 at 07:52
6

According to the discussion here:

the right syntax for Potatoswatter's second solution is:

Foo f( {{1,3}} ); //two braces

a little bit ugly, not consistent with common usage

haohaolee
  • 677
  • 1
  • 9
  • 16
2

You can't, arrays are not like other types (and don't have constructors taking a std::initializer_list).

Try this instead:

struct Foo  
{  
  const std::vector<int>   data;
  Foo(std::initializer_list<int> ini) : data(ini)
  {}
}; 
Bo Persson
  • 90,663
  • 31
  • 146
  • 203
  • 1
    I think it would also work with std::array to get closer to the OPs original implementation. – ronag Apr 05 '11 at 09:14
  • 2
    @ronag: I don't think std::array has any constructors at all, it is supposed to be initialized just like a C style array. – Bo Persson Apr 05 '11 at 09:27
  • 1
    Alas my intended use is in a situation where the overhead of std::vector is unacceptable. std::array would be fine, but it has the exact same problem. – swestrup Apr 05 '11 at 09:35
  • 3
    The C++0x std::array really ought to have an initializer list constructor, as well as a [begin, end) one. The reasons the boost/tr1 implementations do not, stem from C++03 limitations that do not exist in C++0x anymore. – JohannesD Apr 05 '11 at 10:26
  • 1
    @Johannes: `std::tuple`, too. It makes me sad. – Lightness Races in Orbit Apr 05 '11 at 19:29
  • I love this answer. It has all the elements of a good code. Clean, Simple, Understandable, Maintainable and Modern. The thing that impresses me is that @BoPersson answered like this in 2011, the very beginning of modern C++ era! – F14 Jun 22 '21 at 04:29
2

You can define a constexpr function that converts an initializer list to an array. The last (third) function is the one you call. The other create recursively a template parameter pack from the initializer list, and create the array once sufficiently many list elements have been read.

template <typename T, size_t N, typename... Ts>
constexpr enable_if_t<(sizeof...(Ts) == N), array<T, N> >
array_from_initializer_list(const T *const beg, const T *const end,
                            const Ts... xs) {
  return array<T, N>{xs...};
}

template <typename T, size_t N, typename... Ts>
constexpr enable_if_t<(sizeof...(Ts) < N), array<T, N> >
array_from_initializer_list(const T *const beg, const T *const end,
                            const Ts... xs) {
  return array_from_initializer_list<T, N>(beg + 1, end, *beg, xs...);
}

template <typename T, size_t N>
constexpr array<T, N> array_from_initializer_list(initializer_list<T> l) {
  return array_from_initializer_list<T, N>(l.begin(), l.end());
}
Erik Schnetter
  • 482
  • 4
  • 8
  • Isn't that C++14 only? According to cppreference.com, `std::initializer_list::begin` (and `end`) are only `constexpr` starting in C++14. – adentinger Jun 16 '22 at 18:14
0

While this does not work:

#include <initializer_list>
struct Foo
{
  const int data[2];

  constexpr Foo(const std::initializer_list<int>& ini): data{ini}  {}
};

Foo f = {1,3};

I found this simple approach to work nicely:

struct Foo
{
  const int data[2];

  constexpr Foo(const int a, const int b): data{a,b}  {}
};

Foo f = {1,3};

Of course the variadic template approach is probably better if you have a lot of elements, but in this simple case, this will probably suffice.

That is, if you want to explicitly define the constructor from initializer lists. For most POD cases this is fine and dandy:

struct Foo
{
  const int data[2];
};
Foo f = {1,3};
lanwatch
  • 1,377
  • 2
  • 9
  • 9
0

If you don't care about bounds checking, then the following will work.

struct Foo {
    int const data[2];
    Foo(std::initializer_list<int> ini)
        : data{*std::begin(ini), *std::next(std::begin(ini), 1)} {}
};
Felix Glas
  • 15,065
  • 7
  • 53
  • 82