Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How does std::optional construct std::variant in place from initializer list?

Tags:

c++

c++17

#include <optional>
#include <variant>
#include <utility>
#include <set>

int main()
{
    std::optional<std::variant<std::pair<int, int>, std::set<int>>> foo(std::in_place, {1, 4});
}
  1. This compiles. But how is that possible when std::variant does not have a constructor that takes std::initializer_list<T> as its first argument, but all that this constructor overload of std::optional does is pass the initializer list and forward other arguments to the internal type constructor?
  2. Which type does the std::variant hold now, std::set or std::pair?
like image 352
Nikola Pantelic Avatar asked Sep 24 '20 16:09

Nikola Pantelic


People also ask

How does std optional work?

std::optional contains the object within itself, depending on where it is stored (stack/data/heap) std::optional makes a copy of the contained object. Monadic functions will be added in C++23 to improve the abstraction in our code by removing the needs of writing boilerplate code.

How does C++ variant work?

It holds one of several alternatives in a type-safe way. No extra memory allocation is needed. The variant needs the size of the max of the sizes of the alternatives, plus some little extra space for knowing the currently active value. By default, it initializes with the default value of the first alternative.

Does std optional allocate?

What's more, std::optional doesn't need to allocate any memory on the free store. std::optional is a part of C++ vocabulary types along with std::any , std::variant and std::string_view .

Does Optional make a copy?

When you call the function and pass an instance of T, an optional will be constructed which will own its own copy of T, therefore it will call T's copy constructor. In order to be able to call foo and pass a T without creating a copy, you can declare foo as receiving an optional reference, i.e.


1 Answers

It's a bit complicated, so bear with me.

optional<T> has a constructor that takes an in_place_t and an initializer_list<U> (and optional extra arguments). It will then attempt to construct T as if by this statement:

T t(list, ...);

Where list is the initailizer list and ... are any extra arguments.

T is of course a variant<pair, set>. variant is implicitly convertible from any type V through a template constructor. However, this constructor only exists if V is a type such that, given a value v of this type, the following is true:

W w(std::forward<V>(v));

Where W (I'm running out of letters here) is one of the types in the variant. Now variant can have multiple types, so variant basically considers all possible Ws and their constructor overloads. So long as overload resolution selects exactly one such constructor among all possible Ws in the variant, the conversion will work.

pair has no constructor that takes an initializer_list, so none of its constructors count. set<X> has a constructor that takes an initializer_list<X>.

So, back to the original statement. Because optional had an initializer_list<Y> parameter where Y is deduced, the braced-init-list will deduce Y as int. And int is the same type as the set in the variant. So set<int> can be constructed from an initailizer_list<int>.

And therefore, that's what gets created in the variant.

If you had a vector<int> as part of the variant, you'd get a compile error due to the ambiguity of which to call.

And there's not really a way to fix that through in_place gymnastics. You can't do (std::in_place, std::in_place_type<std::set>, {1, 4}), because the braced-init-list won't get properly deduced. So you'd have to do (std::in_place, std::set{1, 4}) and let move construction move from the temporary into the variant.

like image 145
Nicol Bolas Avatar answered Oct 11 '22 15:10

Nicol Bolas