Here is my attempt at explanation:
This line:
Foo f = Foo({{"a", "b"}, {"c", "d"}});
is roughly equivalent to the following:
Foo f =
Foo(
std::initializer_list<std::initializer_list<const char*>>{
std::initializer<const char*>{"a", "b"},
std::initializer<const char*>{"c", "d"}
}
);
Since std::pair has a constructor for any types U1 and U2, std::pair can be constructed from an initializer_list<const char*>, or two const char*. The compiler most likely constructs two std::pair<std::string, std::string>, as it sees that we have an overload for a std::vector of the aforementioned type:
Foo f =
Foo(
std::initializer_list<std::pair<std::string, std::string>>{
std::pair<std::string, std::string>("a", "b"),
std::pair<std::string, std::string>("c", "d")
}
);
Since std::vector<T> has a constructor that takes a std::initializer_list<T>, then the correct overload is chosen.
Let's take a look at this:
Foo g = Foo({{"a", "b"}});
which turns into:
Foo f =
Foo(
std::initializer_list<std::initializer_list<const char*>>{
std::initializer_list<const char*>{"a", "b"}
}
);
Here it is not so obvious what we are doing. The compiler (assuming that we are just idiots) thinks that we might be trying to create a Foo from either an initializer_list<const char*> and then copying that Foo, or that we are directly creating the Foo with an initilizer_list<initializer_list<const char*>>. Since the constructor for std::vector<std::pair<std::string,std::string>> takes a std::initializer_list<std::pair<std::string,std::string>> and not a std::initializer_list<std::initializer_list<const char*>>, it is not clear to a dumb compiler what we are trying to do.
This is wild! However, consider what would happen here if the foo copy constructor was marked as explicit:
Foo f = Foo({{"a", "b"}, {"c", "d"}}); // Compiles with clang and gcc.
// Foo g = Foo({{"a", "b"}});
Foo f2 = Foo({f}); // No errors! Equivalent to Foo{f}
Since Foo({f}) is equivalent to Foo{f}, then g could be considered the same as:
Foo g = Foo{{"a", "b"}};
The compiler won't do the same for f because there is no foo constructor that takes two arguments (i.e., {"a","b"} and {"c","d"}).
In addition, consider what happens when you explicitly construct a std::string from "a" and "b":
Foo g = Foo({{std::string("a"), std::string("b")}});
This runs just fine. That is because the compiler can clearly see that std::vector<std::pair<std::string, std::string>> is a better choice than Foo because std::pair can be constructed from two std::string's
In conclusion:
One of the two things is most likely true:
- GCC simply is confused about what we are trying to do, as it sees
Foo(const Foo&) and Foo(const std::vector<std::pair<std::string,std::string>>&) both take one
argument and both seem to be equally valid.
- Since
Foo({...}) is equivalent to Foo{...}, the compiler is confused by the fact that we are calling Foo{{"a","b"}}, trying to create a std::vector<std::pair<std::string,std::string>> from "a" and "b".
The solutions:
- Use
Foo{{{"a", "b"}}} instead; or:
- Put
using namspace std::string_literals in your code and replace Foo({{"a", "b"}}) with Foo({{"a"s, "b"s}}); or:
- Use
std::make_pair
EDIT: One more important point: if you delete the Foo copy constructor, it becomes apparent that the compiler is trying to create a pair from "a" and another from "b". Take a look at the error message for this code:
#include <iostream>
#include <utility>
#include <vector>
#include <string>
class Foo {
public:
Foo(const std::vector< std::pair<std::string, std::string> >& list) {}
Foo(const Foo&) = delete;
};
int main() {
Foo foo1({ {"a", "b"}, {"c", "d"} });
Foo foo2({ {"a", "b"} });
}
error: no matching function for call to ‘std::pair,
std::__cxx11::basic_string >::pair(const char&)’
PLEASE NOTE: this is just a combination of what I know about std::initializer_list's and my inferences about what is happening. Take this answer with a grain of salt, what is possibly occurring.