Curious Curious - 3 months ago 5
C++ Question

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

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)

Answer

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.