8

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 ?

15
  • 7
    This is really to broad and depends very much on use case. But I wouldn't really recommend std::initializer_list for anything other than as for arguments to functions. Commented May 16, 2019 at 11:54
  • 8
    My main concern would be about readability - I'd be pretty surprised to see std::initializer_list used for something that is not initialization. Commented May 16, 2019 at 11:55
  • 6
    You also forgot about std::vector and std::array in your comparison. Most notably std::array is an aggregate just like the C-style array, and it's copy- and move-able, which plain C-style arrays aren't. Commented May 16, 2019 at 11:55
  • 1
    @Yksisarvinen, good point, as for surprise, I do intent to use with a constructor of a std container - just not immediately. Commented May 16, 2019 at 12:22
  • 1
    FWIW, I prefer constexpr auto arr = std::array{X{5, 6.3}};. I'm a fan of the almost always auto idiom and CTAD makes getting a std::array really easy. Commented May 16, 2019 at 12:33

3 Answers 3

5

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..

Sign up to request clarification or add additional context in comments.

8 Comments

I hate to ask, but what's so wrong about my answer that it deserves a downvote? I'd like to fix. Thanks.
I'm trying to understand it from ten minutes. It seems to me an interesting answer (upvoted). I just ask you a sub-question (maybe you can elaborate a little): why do you say that intializer_list is "far simpler and more flexible"?
I downvoted mainly because it's not very objective and starts out explaining something we already know in a loaded way... , (this wasn't a question of style or opinion, and if you noticed it I put constexpr in front of both expressions ) and otherwise doesn't go into any details, give examples, reference documentation or guidelines, etc. etc. - overall not very usefull
@darune My answer is concise, certainly objective (show me where I stated personal opinion) and less 'loaded' than the two page explanation you accepted. I don't need to give reference because I consider my explanations authoritative as they are. The lack of detail is simply a contrast to the bloated answers surrounding it. "Anyway, I point your attention regarding a point." Totally not loaded.
@max66 It's simpler and more flexible as it doesn't require you to think of, or constrain length the same way an array parameter would.
|
2

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.

3 Comments

I was under the impression that range-based for loop should work fine for array types. see en.cppreference.com/w/cpp/language/range-for
If range_expression is an expression of array type, then begin_expr is __range and end_expr is (__range + __bound), where __bound is the number of elements in the array (if the array has unknown size or is of an incomplete type, the program is ill-formed)
@darune - Yes: they works perfectly also for C-style arrays. Is the other solution that works in a simple way with C-style arrays but it's complicated to use it with initializer list; see also the part of the answer I've added for your conversion question: the std::index_sequence solution permit to trivially convert a C-style array to an intializer list; the contrary is more complicated.
1

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.

4 Comments

will you be able to use/convert the one type to the other type ?
@darune you can copy the elements of an initializer_list into an array. I'm not sure about reverse conversion. Possibly using some dark template magic.
I guess another very strong point about the initializer_list is that we do not have to resort to templates for when passing into a function, etc. that makes writing interfaces clear and to the point without extra 'template overhead'.
@darune I added a quick mention of that. Max66's answer already covers that in more depth.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.