Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

C++ Constructor is ambiguous with std::map of the same key/value types

Tags:

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 ints, chars, 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?

like image 832
acikek Avatar asked Feb 28 '21 00:02

acikek


2 Answers

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:

  • outer braces initializes a map using the map's initializer-list constructor, inner braces initializes a pair for that initializer-list constructor.
  • outer braces initializes a 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.

like image 103
T.C. Avatar answered Oct 20 '22 00:10

T.C.


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++)

like image 21
Lionell Loh Jian An Avatar answered Oct 19 '22 23:10

Lionell Loh Jian An