"ambiguous overload for 'operator[]'" if conversion operator to int exist

As noted in other answers, your problem is that [] commutes by default -- a[b] is the same as b[a] for char const*, and with your class being convertible to uint32_t this is as good a match as the char* being converted to std::string.

What I'm providing here is a way to make an "extremely attractive overload" for when you are having exactly this kind of problem, where an overload doesn't get called despite your belief that it should.

So here is a Foo with an "extremely attractive overload" for std::string:

struct Foo
{
  operator uint32_t() {return 1;}
  Foo& lookup_by_string(const std::string &foo) { return *this; }
  Foo& operator[](size_t index) {return *this;}
  template<
    typename String,
    typename=typename std::enable_if<
      std::is_convertible< String, std::string >::value
    >::type
  > Foo& operator[]( String&& str ) {
    return lookup_by_string( std::forward<String>(str) );
  }
};

where we create a free standing "lookup by string" function, then write a template that captures any type that can be converted into a std::string.

Because it "hides" the user-defined conversion within the body of the template operator[], when checking for matching no user defined conversion occurs, so this is preferred to other operations that require user defined conversions (like uint32_t[char*]). In effect, this is a "more attractive" overload than any overload that doesn't match the arguments exactly.

This can lead to problems, if you have another overload that takes a const Bar&, and Bar has a conversion to std::string, the above overload may surprise you and capture the passed in Bar -- both rvalues and non-const variables match the above [] signature better than [const Bar&]!


The problem is that your class has a conversion operator to uint32_t, so the compiler does not know whether to:

  1. Construct a std::string from the string literal and invoke your overload accepting an std::string;
  2. Convert your Foo object into an uint32_t and use it as an index into the string literal.

While option 2 may sound confusing, consider that the following expression is legal in C++:

1["foo"];

This is because of how the built-in subscript operator is defined. Per Paragraph 8.3.4/6 of the C++11 Standard:

Except where it has been declared for a class (13.5.5), the subscript operator [] is interpreted in such a way that E1[E2] is identical to *((E1)+(E2)). Because of the conversion rules that apply to +, if E1 is an array and E2 an integer, then E1[E2] refers to the E2-th member of E1. Therefore, despite its asymmetric appearance, subscripting is a commutative operation.

Therefore, the above expression 1["foo"] is equivalent to "foo"[1], which evaluates to o. To resolve the ambiguity, you can either make the conversion operator explicit (in C++11):

struct Foo
{
    explicit operator uint32_t() { /* ... */ }
//  ^^^^^^^^
};

Or you can leave that conversion operator as it is, and construct the std::string object explicitly:

    f[std::string("foo")];
//    ^^^^^^^^^^^^     ^

Alternatively, you can add a further overload of the subscript operator that accepts a const char*, which would be a better match than any of the above (since it requires no user-defined conversion):

struct Foo
{
    operator uint32_t() { /* ... */ }
    Foo& operator[](const std::string &foo) { /* ... */ }
    Foo& operator[](size_t index) { /* ... */ }
    Foo& operator[](const char* foo) { /* ... */ }
    //              ^^^^^^^^^^^
};

Also notice, that your functions have a non-void return type, but currently miss a return statement. This injects Undefined Behavior in your program.


The problem is that f["foo"] can be resolved as:

  1. Convert "foo" to std::string (be it s) and do f[s] calling Foo::operator[](const std::string&).
  2. Convert f to integer calling Foo::operator int() (be it i) and do i["foo"] using the well known fact that built-in [] operator is commutative.

Both have one custom type conversion, hence the ambiguity.

The easy solution is to add yet another overload:

Foo& operator[](const char *foo) {}

Now, calling f["foo"] will call the new overload without needing any custom type conversion, so the ambiguity is broken.

NOTE: The conversion from type char[4] (type type of "foo") into char* is considered trivial and doesn't count.

Tags:

C++