Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why is this C++ expression involving overloaded operators and implicit conversions ambiguous?

operator bool breaks the use of operator< in the following example. Can anyone explain why bool is just as relevant in the if (a < 0) expression as the specific operator, an whether there is a workaround?

struct Foo {
    Foo() {}
    Foo(int x) {}

    operator bool() const { return false; }

    friend bool operator<(const Foo& a, const Foo& b) {
        return true;
    }
};

int main() {
    Foo a, b;
    if (a < 0) {
        a = 0;
    }
    return 1;
}

When I compile, I get:

g++ foo.cpp
foo.cpp: In function 'int main()':
foo.cpp:18:11: error: ambiguous overload for 'operator<' (operand types are 'Foo' and 'int')
     if (a < 0) {
           ^
foo.cpp:18:11: note: candidate: operator<(int, int) <built-in>
foo.cpp:8:17: note: candidate: bool operator<(const Foo&, const Foo&)
     friend bool operator<(const Foo& a, const Foo& b)
like image 911
jkj yuio Avatar asked Aug 28 '17 11:08

jkj yuio


2 Answers

The important points are:

First, there are two relevant overloads of operator <.

  • operator <(const Foo&, const Foo&). Using this overload requires a user-defined conversion of the literal 0 to Foo using Foo(int).
  • operator <(int, int). Using this overload requires converting Foo to bool with the user-defined operator bool(), followed by a promotion to int (this is, in standardese, different from a conversion, as has been pointed out by Bo Persson).

The question here is: From whence does the ambiguity arise? Certainly, the first call, which requires only a user-defined conversion, is more sensible than the second, which requires a user-defined conversion followed by a promotion?

But that is not the case. The standard assigns a rank to each candidate. However, there is no rank for "user-defined conversion followed by a promotion". This has the same rank as only using a user-defined conversion. Simply (but informally) put, the ranking sequence looks a bit like this:

  1. exact match
  2. (only) promotion required
  3. (only) implicit conversion required (including "unsafe" ones inherited from C such as float to int)
  4. user-defined conversion required

(Disclaimer: As mentioned, this is informal. It gets significantly more complex when multiple arguments are involved, and I also didn't mention references or cv-qualification. This is just intended as a rough overview.)

So this, hopefully, explains why the call is ambiguous. Now for the practical part of how to fix this. Almost never does someone who provides operator bool() want it to be implicitly used in expressions involving integer arithmetic or comparisons. In C++98, there were obscure workarounds, ranging from std::basic_ios<CharT, Traits>::operator void * to "improved" safer versions involving pointers to members or incomplete private types. Fortunately, C++11 introduced a more readable and consistent way of preventing integer promotion after implicit uses of operator bool(), which is to mark the operator as explicit. This will remove the operator <(int, int) overload entirely, rather than just "demoting" it.

As others have mentioned, you can also mark the Foo(int) constructor as explicit. This will have the converse effect of removing the operator <(const Foo&, const Foo&) overload.

A third solution would be to provide additional overloads, e.g.:

  • operator <(int, const Foo&)
  • operator <(const Foo&, int)

The latter, in this example, will then be preferred over the above-mentioned overloads as an exact match, even if you did not introduce explicit. The same goes e.g. for

  • operator <(const Foo&, long long)

which would be preferred over operator <(const Foo&, const Foo&) in a < 0 because its use requires only a promotion.

like image 21
Arne Vogel Avatar answered Oct 01 '22 15:10

Arne Vogel


The problem here is that C++ has two options to deal with a < 0 expression:

  • Convert a to bool, and compare the result to 0 with built-in operator < (one conversion)
  • Convert 0 to Foo, and compare the results with < that you defined (one conversion)

Both approaches are equivalent to the compiler, so it issues an error.

You can make this explicit by removing the conversion in the second case:

if (a < Foo(0)) {
    ...
}
like image 65
Sergey Kalinichenko Avatar answered Oct 01 '22 17:10

Sergey Kalinichenko