copy-list-initialization of std::map with std::variant as mapped_type

Viewed 221

Why this piece of code compiles?

#include <string>
#include <variant>
#include <map>

void foo(std::map<int, std::variant<int, std::string>> map)
{}

int main()
{
    foo({{1,"1"}, {2, 2}});
}

We have {{1,"1"}, {2, 2}} thing which std::map should be initialized with. It probably has something to do with the std::map ctor which accepts std::initializer_list & list-initialization. But I wonder how this thing {{1,"1"}, {2, 2}} may compile at all? What type it has? How overload resolution works in this case?

2 Answers

First, in the example you give, there is no overload resolution. Well, not in the sense that there's any choice in overloads. Either the one function matches, or it doesn't; there's no picking order or templates or any of the complexities normally meant when speaking about "overload resolution".

The compiler tries to "map" {{1,"1"}, {2, 2}} onto a constructor of std::map<int, std::variant<int, std::string>>, which will work with std::map's std::initializer_list<std::pair<const int, std::variant<int, std::string>> constructor (number 5 here, summarily put: a list of the map's elements), but only if each element in that list can be constructed from the nested parts.

Here, two elements, {1,"1"} and {2, 2} need to be "mapped", separately, to a std::pair<const int, std::variant<int, std::string>, using std::pair's aggregate initialization (meaning you can construct a std::pair<T,U> from a braced init statement {t, u} where t is a T and u is a U). The second argument is more complex, being an std::variant. Here, the compiler probably uses deduction guides (although I think this is an implementation detail) to construct the variant object from one of its possible alternative types.

A full overview of initialization can be found here, although several pieces will need to be put together to come up to the above story. Note this is how I understand the process, could be that there are details that are different in the actual implementation or standard that I don't think would matter here, and might even impede understanding what is going on.

Your code boils down to basically this:

std::map<int, std::variant<int, std::string>> map = {{1,"1"}, {2, 2}};

here we have a braced init list on the right of an object. This indeed looks at the left and finds an appropriate constructor.

The one it finds is an initializer list.

So the above then boils down to this:

std::initializer_list<std::pair<const int, std::variant<int, std::string>>> il = {{1,"1"}, {2, 2}}; The braced-init syntax has no type, but can be used to initialize types, including initializer lists.

Here, it treats the inner {} as std::pair<const int, std::variant<int, std::string>, so this boils down to:

std::pair<const int, std::variant<int, std::string>> one = {1,"1"};
std::pair<const int, std::variant<int, std::string>> two = {2,2};

working. This in turn boils down to

std::variant<int,std::string> v1 = "1";
std::variant<int,std::string> v2 = 2;

working, which finally boils down to the constructor of std::variant doing some SFINAE or requires magic to pick out the "right" constructor.

Related