Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What are pros and cons of std::initializer_list and c array []?

Suppose I have some hypothetic struct:

struct X {
  int i;
  double d;
}

I may then write

constexpr X x_c_array[]{{5, 6.3}};

or

constexpr std::initializer_list<X> x_ilist{{5, 6.3}};

Using auto is not possible - the compiler must know the inner type.

Are there any downsides to either version ?

Update:

Also of concern is if you will be able to use/convert the one type to the other type - eg. when constructing standard containers ?

like image 606
darune Avatar asked May 16 '19 11:05

darune


3 Answers

Plain and simple: initializer_list isn't a container. It's an immutable view onto externally allocated elements. It's utterly unsuitable for any scenario a container would be useful in—consider the needless indirection (no resizability), the immutability, the idiomacy of its name. On top of that, it has no proper interface.

A situation where both seem adequate is a constructor parameter for a sequence. If the length is fixed (or template-parametrized), then int const (&arr)[N] is possible, although initializer_list is far simpler and more flexible. After all, that's what it was designed and intended for..

like image 134
Columbo Avatar answered Oct 13 '22 13:10

Columbo


As written in comments, it's a broad argument.

Anyway, I point your attention regarding a point.

In first case

X x1[] {{5, 6.3}};

the number of element of x1 is part of the x1 type.

So you have that

X x1[] {{5, 6.3}};
X x2[] {{5, 6.3}, {7, 8.1}};

static_assert( false == std::is_same<decltype(x1), decltype(x2)>::value );

Using an initializer list

std::initializer_list<X> x3 {{5, 6.3}};
std::initializer_list<X> x4 {{5, 6.3}, {7, 8.1}};

static_assert( true == std::is_same<decltype(x3), decltype(x4)>::value );

the type remain the same changing the number of elements.

According to your needs this can be an advantage for first or second solution.

The fact that the number of elements is part of the type for C-style arrays can be a little advantage in meta programming.

Suppose you want a function that return the sum of the i values of the arrays, with C-style array you can write

template <std::size_t N, std::size_t ... Is>
constexpr auto sum_i_helper (X const (&xArr)[N], std::index_sequence<Is...>)
 { return (... + xArr[Is].i); }

template <std::size_t N>
constexpr auto sum_i (X const (&xArr)[N])
 { return sum_i_helper(xArr, std::make_index_sequence<N>{}); }

and this function compile also when the argument of sum_i() is a not-constexpr value.

If you wan to write something similar with std::initializer_list is a little more complicated because the size() of the list isn't necessarily a compile-time known value so or you pass it as template parameter (but the function doesn't works with run-time lists) or you use size() inside the function, but you can't use it to initialize a std::index_sequence.

Anyway, with initializer list, you can use the good old for() cycle

constexpr auto sum_i (std::initializer_list<X> const lx)
 { 
   int ret { 0 };

   for ( auto const & x : lx )
      ret += x.i;

   return ret;
 }

and the function can compute compile time when lx is a constexpr value.

Also of concern is if you will be able to use/convert the one type to the other type - eg. when constructing standard containers ?

Convert an array to an initializer list it's easy an works with both compile-time and run-time known value

template <std::size_t N, std::size_t ... Is>
constexpr auto convertX_h (X const (&xArr)[N], std::index_sequence<Is...>)
 { return std::initializer_list<X>{ xArr[Is]... }; }

template <std::size_t N>
constexpr auto convertX (X const (&xArr)[N])
 { return convertX_h(xArr, std::make_index_sequence<N>{}); }

// ....

X x1[] {{5, 6.3}};

std::initializer_list<X> x5 = convertX(x1);

Converting an initializer list to a C-style array is more difficult because the type of the array depends from the number of elements so you need to know compile-time the number of elements in the initializer list, because you can't randomly access to an initializer list and, worse, because you can't write a function that return a C-style array.

I can imagine a solution as follows that convert an initializer list to a std::array (off topic suggestion: use std::array, instead of a C-style array, when possible)

template <std::size_t N>
constexpr auto convertX (std::initializer_list<X> const lx)
 { 
   std::array<X, N> ret;

   std::size_t i { 0u };

   for ( auto const & x : lx )
      ret[i++] = x;

   return ret;
 }

// ...

constexpr std::initializer_list<X> x4 {{5, 6.3}, {7, 8.1}};

auto x6 = convertX<x4.size()>(x4);

but x6 now is a std::array<X, 2>, not a X[2], and x4 must be a constexpr value.

like image 23
max66 Avatar answered Oct 13 '22 13:10

max66


An array can be non-const. initializer_list only allows const access to the elements. In your example you use constexpr and therefore implicitly const, so in that case this doesn't matter. But if you need non-const, the initializer_list is not an option. This is particularly annoying in initializer_list constructors where you'd want to move the elements out from the list, but cannot because the objects are const.

The C style array can decay to pointer to first element, which beginners occasionally find confusing. initializer_list does not. You could use an array wrapper instead which also doesn't decay, but you need to either specify the type and size, or use the type in the brace init list to allow template deduction:

constexpr std::array x_std_array{X{5, 6.3}};

And, as explored more in depth in max66's answer, initializer_list can be of any size, while the size of an array depends on its type, which is either an advantage or a disadvantage. Size part of the type is an advantage in template meta programming, while "hidden" size is an advantage since you don't need a template in the first place.

like image 20
eerorika Avatar answered Oct 13 '22 15:10

eerorika