Here's a class definition as an example.
#include <string>
#include <map>
template <class T>
class Collection
{
private:
std::map<std::string, T> data;
public:
Collection() {}
Collection(std::map<std::string, T> d)
{
data = d;
}
};
This works fine when initializing Collections with int
s, char
s, and even vector
templated types. However, when initializing a one with a string
and calling the second overloaded constructor, like:
Collection<std::string> col({
{ "key", "value" }
});
It does not compile, and throws this exit error:
main.cpp:24:22: error: call to constructor of 'Collection<std::__cxx11::string>'
(aka 'Collection<basic_string<char> >') is ambiguous
Collection<string> col({
^ ~
main.cpp:8:7: note: candidate constructor (the implicit move constructor)
class Collection
^
main.cpp:8:7: note: candidate constructor (the implicit copy constructor)
main.cpp:16:3: note: candidate constructor
Collection(map<string, T> d)
^
The strange thing is, while this notation is fine with other types, and this breaks, this notation works for string
:
Collection<std::string> col(std::map<std::string, std::string>({
{ "key", "value" }
}));
What's going on here?
This is a fun one.
A map
can be constructed from two iterators:
template<class InputIterator>
map(InputIterator first, InputIterator last,
const Compare& comp = Compare(), const Allocator& = Allocator());
Notably, this constructor is not required to check that InputIterator
is an iterator at all, let alone that the result of dereferencing it is convertible to the map
's value type. Actually trying to construct the map will fail, of course, but to overload resolution, map
is constructible from any two arguments of the same type.
So with
Collection<std::string> col({
{ "key", "value" }
});
The compiler sees two interpretations:
map
using the map
's initializer-list constructor, inner braces initializes a pair
for that initializer-list constructor.Collection
, inner braces initializes a map
using the "iterator-pair" constructor.Both are user-defined conversions in the ranking, there is no tiebreaker between the two, so the call is ambiguous - even though the second, if chosen, would result in an error somewhere inside map
's constructor.
When you use braces on the outermost layer as well:
Collection<std::string> col{{
{ "key", "value" }
}};
There is a special rule in the standard that precludes the second interpretation.
In this case, you are missing a {} that encloses the map {{ "key", "value" }}
EDIT: Sorry I can't comment on T.C's answer because of insufficient reputation. In any case, thanks for brilliantly highlighting the point of ambiguity.
I wanted to add on to their answer - to give a complete picture of why constructing with {} does not result in this ambiguity but constructing with () does.
The key difference between braced and parentheses initialization is that during constructor overload resolution, braced initializers are matched to std::initializer_list parameters if at all possible, even if other constructors offer better matches. This is why constructing with {} can resolve the ambiguity.
(This is taken from Item 7 of Scott Myers' Effective Modern C++)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With