19

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)
Sergey Kalinichenko
  • 714,442
  • 84
  • 1,110
  • 1,523
jkj yuio
  • 2,543
  • 5
  • 32
  • 49

4 Answers4

25

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)) {
    ...
}
Sergey Kalinichenko
  • 714,442
  • 84
  • 1,110
  • 1,523
  • convert `a` to `bool`, yes but `bool` is not the same as `int`, – jkj yuio Aug 28 '17 at 11:26
  • 3
    @jkjyuio Zero is not only an `int`, C++ treats it as any type to which it could be converted, including `int`, `bool`, pointers, floats, doubles, and so on. When zero is treated as `bool`, it's the same as `false`. It does not count as a conversion. – Sergey Kalinichenko Aug 28 '17 at 11:38
  • 7
    More precisely, the conversion from `bool` to `int` is an *integral promotion* rather than a *conversion*. See section 13.3.3.2 [over.ics.rank] in (for example) [n4296](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2014/n4296.pdf). Yes, it is horribly complicated. Avoid implicit conversions! – Martin Bonner supports Monica Aug 28 '17 at 12:02
13

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.

Arne Vogel
  • 6,346
  • 2
  • 18
  • 31
  • Thank you very much for this detailed answer. I am marking this as the correct one. Yes, my questing was with regard to the ranking of the match, as it seemed bool->int was an _additional_ conversion. But it is explained in terms of _promotion_. Additionally, the fix to to have `explicit operator bool` indeed, you would almost never want further promotion. Thanks to others too for their input. – jkj yuio Aug 28 '17 at 15:30
4

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.

WindyFields
  • 2,697
  • 1
  • 18
  • 21
  • no because the compiler did not list operator<(bool,int). only (Foo, Foo) and (int, int). – jkj yuio Aug 28 '17 at 11:24
  • In your case indeed, `operator<(int, int)`. However, MSVC 2017 does lists `operator<(bool,int)`. – WindyFields Aug 28 '17 at 11:28
  • Does this mean converting `bool` to `int` does not count as a conversion step.so they are the _same_ type? – jkj yuio Aug 28 '17 at 11:30
  • I am not sure about a conversion step, but `int` and `bool` are definitely different types in Standard C++ (although, in C and probably pre-Standard C++ there were no `bool`, only `int`) – WindyFields Aug 28 '17 at 11:40
2

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.

Dusteh
  • 1,496
  • 16
  • 21
  • Why is comparing `bool` vs `int` not counted as an _additional_ conversion? – jkj yuio Aug 28 '17 at 11:27
  • 4
    @jkj - `bool` to `int` is a "promotion" and not a conversion. :-) Note that the chapter "Standard conversions" takes 7 pages to explain in the standard document. A full explanation doesn't fit on StackOverflow. Your basic problem is that your class offers implicit conversions both to and from the class. Most people avoid doing that for the precise reason you have noticed - it gets confusing both for the readers and for the compiler. – Bo Persson Aug 28 '17 at 12:12
  • @Bo Persson, thanks for pointing this out. Arne Vogel has explained it in detail above. – jkj yuio Aug 28 '17 at 15:31