10

Why does constructor (4) exist for std::variant from http://en.cppreference.com/w/cpp/utility/variant/variant? It seems like it is going to cause a lot of ambiguity in code that could otherwise have been avoided by being explicit.. For example the code sample on cppreference highlights a possible ambiguity that users might not notice (the third line)

variant<string> v("abc"); // OK
variant<string, string> w("abc"); // ill-formed, can't select the alternative to convert to
variant<string, bool> w("abc"); // OK, but chooses bool

Is there some case where it is absolutely going to be needed?

The other question was why constructors (6) and (8) are needed from the same cppreference page. Won't (5) and (7) serve the purposes that (6) and (8) are meant for? I might be misunderstanding their usage..


For the reader the constructors that I referred to in my question are

constexpr variant();              // (1)    (since C++17)

variant(const variant& other);    // (2)    (since C++17)

variant(variant&& other);         // (3)    (since C++17)

template< class T >               // (4)    (since C++17)
constexpr variant(T&& t);

template< class T, class... Args >
constexpr explicit variant(std::in_place_type_t<T>, Args&&... args); // (5) (since C++17)

template< class T, class U, class... Args >
constexpr explicit variant(std::in_place_type_t<T>,
                           std::initializer_list<U> il, Args&&... args); // (6) (since C++17)

template< std::size_t I, class... Args >
constexpr explicit variant(std::in_place_index_t<I>, Args&&... args) // (7) (since C++17)

template <size_t I, class U, class... Args>
constexpr explicit variant(std::in_place_index_t<I>,
                           std::initializer_list<U> il, Args&&... args); // (8) (since C++17)
Ryan Haining
  • 35,360
  • 15
  • 114
  • 174
Curious
  • 20,870
  • 8
  • 61
  • 146
  • Well, do you really want to use the `in_place` constructor *every* time you initialize a variant with one of the bounded types? – Brian Bi Aug 30 '16 at 22:15
  • 1
    As for the initializer_list constructors, I'm pretty sure those are needed if you ever want to pass a braced-init-list directly, rather than constructing an initializer_list object and then passing it in. – Brian Bi Aug 30 '16 at 22:17
  • @Brian personally, it seems like for this interface. I would think that is the best way to go about constructing a variant. Because (4) can be ambiguous a lot of times... – Curious Aug 30 '16 at 22:18
  • @Brian about the initializer_list constructors, weird I never thought that would be an issue... – Curious Aug 30 '16 at 22:21
  • 2
    I don't agree with the reasoning that just because (4) can be ambiguous, we should make everyone's lives harder in order to avoid this. C++ isn't that type of language. – Brian Bi Aug 30 '16 at 22:21

1 Answers1

9

Is there some case where it is absolutely going to be needed?

No. But things don't get added because they are "absolutely going to be needed". They get added because they are useful.

And being implicitly convertible from one of its component types is very useful for a variant. Yes, it creates ambiguity in some corner cases. But this ambiguity is usually due to defects in type design (like string literals preferring to convert to bool over user-defined conversions).

If there is an ambiguous case, then you simply have to be explicit about it. Like using "abc"s UDL literals rather than naked string literals (yet another reason to do so). But there's no reason to force everyone to be explicit when you're dealing with well-designed types.

Won't (5) and (7) serve the purposes that (6) and (8) are meant for?

Not in a reasonable way.

In every case in the standard, when a function takes variadic arguments that will be passed to a constructor, they will use constructor syntax rather than {} syntax on that object. So if you have this:

using type = vector<int>;
variant<type> t(in_place<type>, 6);

You will get a call to vector<int>(6). Note that this is different from vector<int>{6}. That is, you don't get initializer list constructors unless you actually pass an initializer list.

Now, you could do:

variant<type> t(in_place<type>, initializer_list<int>{6});

But that's excessively verbose. By contrast:

variant<type> t(in_place<type>, {6});

That's far less verbose. The compiler can deduce the type of the initializer list. Whereas template argument type deduction fails if you try to deduce a braced-init-list as an arbitrary T.

Among other ways template deduction differs from deduction with auto because it does not deduce initializer_lists from braced-init-list expressions. For example

template <typename Type>
void func(const Type&);

will not deduce Type to be an std::initializer_list for the following call

func({1, 2, 3, 4, 5});

for more information about this see see Universal references and std::initializer_list.

Community
  • 1
  • 1
Nicol Bolas
  • 449,505
  • 63
  • 781
  • 982
  • Why does deduction fail when you try and deduce a `braced-init-list` as an arbitrary `T`? I thought it should default to an `initializer_list`? I say this because `auto` with a `braced-init-list` defaults to an `initializer_list` – Curious Aug 30 '16 at 22:26
  • @Curious That's a separate question, and should be asked separately. I also want to know the answer, myself. – Brian Bi Aug 30 '16 at 22:30
  • @Brian posting now. I'll link you to it as soon as I am done – Curious Aug 30 '16 at 22:31
  • 3
    @Curious actually I realized this must have been asked before; http://stackoverflow.com/a/17622767/481267 – Brian Bi Aug 30 '16 at 22:33