Questions about three constructors in std::variant's proposed interface
Asked Answered
P

1

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)
Pneumonic answered 30/8, 2016 at 21:55 Comment(5)
Well, do you really want to use the in_place<T> constructor every time you initialize a variant with one of the bounded types?Adams
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.Adams
@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...Pneumonic
@Brian about the initializer_list constructors, weird I never thought that would be an issue...Pneumonic
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.Adams
B
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.

Bolivar answered 30/8, 2016 at 22:21 Comment(4)
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_listPneumonic
@Pneumonic That's a separate question, and should be asked separately. I also want to know the answer, myself.Adams
@Brian posting now. I'll link you to it as soon as I am donePneumonic
@Pneumonic actually I realized this must have been asked before; https://mcmap.net/q/303291/-why-do-auto-and-template-type-deduction-differ-for-braced-initializersAdams

© 2022 - 2024 — McMap. All rights reserved.