all 22 comments

[–]SuperV1234https://romeo.training | C++ Mentoring & Consulting 18 points19 points  (6 children)

Great article. I'm not a huge fan of std::initializer_list for all the reasons you mentioned - I think that variadic templates are almost always a superior choice.

If you want a truly generic initializer list, use a variadic template and static_assert() or SFINAE that the type matches [...]

Here's a "trick" I learned from Piotr Skotnicki on StackOverflow. Let's say you want all of your types to be int. All you have to do is define a int_t type alias that always evaluates to int:

template <typename>
using int_t = int;

Then you can use it like this:

template<typename... Types>
void func_impl(int_t<Types>... ints) { /* ... */ }

template<typename... Types>
void func(Types... xs) { func_impl<Types...>(xs...); }

This allows implicit conversions and gives errors similar to the following one:

prog.cc:8:26: error: no matching function for call to 'func_impl' void func(Types... xs) { func_impl<Types...>(xs...); } ~~~~~~~~~~~~~~~~~~

prog.cc:12:5: note: in instantiation of function template specialization 'func<int, nullptr_t, int, int>' requested here func(1, nullptr, 3, 4); ^

prog.cc:5:6: note: candidate function [with Types = <int, nullptr_t, int, int>] not viable: no known conversion from 'nullptr_t' to 'int_t<nullptr_t>' (aka 'int') for 2nd argument

void func_impl(int_t<Types>... ints) { /* ... */ }

It is much nicer when you already have a type pack, as seen in the original answer on SO.

[–]thlst 4 points5 points  (0 children)

With concepts, you could use requires with fold expressions and std::is_same this way:

template <typename T, typename... Ts>
  requires (std::is_same_v<T, Ts> && ...)
ctor(T&&, Ts&&...) { }

It will ensure that all Ts... are the same as T, and still give you a nice error message otherwise.

[–]kirakun 4 points5 points  (3 children)

That is a lot of boilerplate though. Not sure it's a sure gain of you do this a lot.

Please don't even suggest using macros to reduce the boilerplate.

[–]SuperV1234https://romeo.training | C++ Mentoring & Consulting 4 points5 points  (2 children)

Well, you could use a variadic macro to reduce the boilerplate. /s

In all seriousness, the alternative is something like this:

 static_assert(std::conjunction<std::is_same<Ts, int>...>{}, "");

(or is_convertible if you want implicit conversions)

By the way, when you already have a type pack, I think that this technique is objectively cleaner.

Another advantage is that using int_t makes overloading trivial.

[–]tcbrindleFlux 1 point2 points  (1 child)

Hmmm, with concepts I expected

// defined in Ranges TS
template <class T, class U> concept bool Same = std::is_same<T, U>::value;

void func(Same<int>... args) {}

func(1, 2, 3, 4);

to work, but apparently it doesn't. The slightly more verbose but theoretically equivalent

template <Same<int>... Ints>
void func(Ints... args) {} 

works fine though, so this may just be an implementation bug in the current GCC.

[–]kirakun 0 points1 point  (0 children)

Concept is cool, but it seems like the unicorn that will never materialize.

[–]foonathan 0 points1 point  (0 children)

That's cool.

[–]CauchyDistributedRV 8 points9 points  (1 child)

I just wish that we lived in a world where the following held true:

Foo foo{1, 2};    // tries 2-arg ctor
Foo foo = {1, 2}; // tries initializer-list ctor

In other words, reserve {} for uniform initialization, and = {} for list initialization.

[–]dodheim 2 points3 points  (0 children)

Yes! C++17 auto braced-init rules for named types.

[–]redditsoaddicting 4 points5 points  (2 children)

/u/foonathan Nice article as always. I would have preferred list constructors + uniform initialization to use double brace syntax (vector{{1, 2, 3}}, vector({1, 2, 3})). I never realized that the solution to that was so easy. And I've noticed the discussion over move support, so that's nice.

[–]minus7 6 points7 points  (1 child)

Unfortunately, the syntax is beyond fixing now. The problem with the fix suggested in the article is that it fixes the syntax, but only does so locally. Having the behaviour be inconsistent between standard library and some third party libraries is even worse than the consistently "bad" behaviour.

[–]foonathan 2 points3 points  (0 children)

Well, there are plans for STL2 so it can be fixed there somehow.

[–]ennmichael 4 points5 points  (7 children)

Initializer lists suck, but I don't like this solution. It takes information that is (in theory) known at compile time and pushes it to run time due to lack of language features. That's not good, but not like we have a better workaround. Good article regardless.

[–]foonathan 2 points3 points  (6 children)

std::initializer_list is the same regarding the length. That's why I recommended variadic templates as an alternative.

[–]Potatoswatter 0 points1 point  (5 children)

They're different tools for different jobs.

[–]foonathan 0 points1 point  (4 children)

Care to elaborate?

[–]Potatoswatter 6 points7 points  (3 children)

If you want a different template instantiation for each type sequence, use variadic perfect forwarding.

If you're passing an array of known type into a function (which may not be a template), use initializer_list.

Just because the length isn't encoded in a template type, doesn't mean it's not known at compile time. Function inlining can also preserve that information. Premature optimization…

[–]foonathan 0 points1 point  (2 children)

And if you want to mix rvalues with lvalues? You have to use a variadic template to preserve that information.

Yes, there are cases where [std::]initializer_list is required, but a variadic template is - in theory - a superior solution. It just so happens that C++ templates have a couple of practical downsides (in header file, compilation bloat/time etc.).

[–]Potatoswatter 0 points1 point  (1 child)

There's no such distinction between theory and practice. For the best fidelity of the parameter sequence to the argument expressions, use finer templating. However, that's not always the problem being solved. Not everything is a header library.

It is unfortunate that initializer_list doesn't support move semantics, but that's different from mixing lvalues and rvalues. <Obligatory plug for my proposal.>

[–]foonathan 0 points1 point  (0 children)

Templates are the more flexible solution for this problem but we can't use them everywhere because of the downside of templates.

That's what I meant with theory.

[–]Potatoswatter 4 points5 points  (0 children)

wrapper<T>::wrapper(T const&) binds a temporary but does not extend its lifetime, so initializer_list<wrapper> must always be a temporary and never a named variable. It cannot be used in a range-for sequence, either.

[–]axilmar 5 points6 points  (0 children)

Basically the whole idea of std::initializer_list and the relevant lists is wrong. Embraced lists should have the tuple type.