简体   繁体   中英

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)

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)) {
    ...
}

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, eg:

  • 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 eg 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.

Because compiler can not choose between bool operator <(const Foo &,const Foo &) and operator<(bool, int) which both fits in this situation.

In order to fix the issue make second constructor explicit :

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

    operator bool() const { return false; }

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

Edit: Ok, at last I got a real point of the question :) OP asks why his compiler offers operator<(int, int) as a candidate, though "multi-step conversions are not allowed" .

Answer: Yes, in order to call operator<(int, int) object a needs to be converted Foo -> bool -> int . But , C++ Standard does not actually say that "multi-step conversions are illegal".

§ 12.3.4 [class.conv]

At most one user-defined conversion (constructor or conversion function) is implicitly applied to a single value.

bool to int is not user-defined conversion, hence it is legal and compiler has the full right to chose operator<(int, int) as a candidate.

This is exactly what the compiler tells you.

One approach for solving the if (a < 0) for the compiler is to use the Foo(int x) constructor you've provided to create object from 0.

The second one is to use the operator bool conversion and compare it against the int (promotion). You can read more about it in Numeric promotions section.

Hence, it is ambiguous for the compiler and it cannot decide which way you want it to go.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM