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:
- exact match
- (only) promotion required
- (only) implicit conversion required (including "unsafe" ones inherited from C such as
float
to int
)
- 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.