The other day I was chatting to Rich Talbot-Watkins about C++ stuff and he sent me some example code which shows how unusual the C++ function overloading rules can be.
Take the example code snippet1:
What would you expect to happen? When compiled on Microsoft Visual Studio 2005, main()
returns true
. The type anEnum
is promoted to int
, and then the enum
value (implicitly zero as it’s the first enumeration entry) compares with the zero char value.
Nothing too surprising there, you might think.
Let’s see what happens on GCC:
error: ISO C++ says that these are ambiguous, even
though the worst conversion for the first is
better than the worst conversion for the second:
note: candidate 1: operator==(int, int) <built-in>
note: candidate 2: bool operator==(char, MyClass)
Ah…oh dear. The C++ standards say that when looking for which function to call all possible conversion paths must be considered. There are then complex and tricky rules as to which is the “best” choice, or even if there exists an unambiguous “best” choice.
In this case there’s two ways to compare a char
with a MyEnum
. The
first is perhaps the most obvious one[^2]:
char
gets converted to int
(4.5 paragraph 1.)MyEnum
gets converted to int
(4.5 paragraph 2.)operator==(int,int)
) between two integers can be used to compare the values.The second, less obvious way of comparing the two values is:
char
.MyEnum
gets converted to double
(4.9 paragraph 2.)double
gets converted (via the constructor) to MyClass
.operator==(char,MyClass)
can now be used to compare the values.Given these two choices, which is best? This is covered in 13.3.3 — one of the most complex bit of standardese I’ve ever looked at. This determines whether one function might be better than another when choosing between them for function overloading. Quoting the relevant bits of the standards document here:
“Let ICS_i_(F) denote the implicit conversion sequence that converts the i-th argument in the list to the type of the i-th parameter of [a function]…”
“Given these definitions, a viable function F1 is defined to be a better function than another viable function F2 if for all arguments i, ICS_i_(F1) is not a worse conversion sequence than ICS_i_(F2), and then…[cut for brevity]”
Let’s say that F1 is operator==(int,int)
and F2 is operator==(char,MyClass)
. So
by this first part of the standard, could F1 be a better choice than F2? To find out,
we compare the conversion sequences needed to make the actual
types specified (char
and MyEnum
) fit the viable functions’ expected parameters (13.3.3.2):
Parameter 1 is a char
. F1 requires an int
, F2 requires a char
.
char
→int
vs char
.
The latter is an “Exact Match”, and so F2 is better than F1 on the basis of this parameter.
Parameter 2 is a MyEnum
. F2 requires an int
, F2 requires a MyClass
MyEnum
→int
vs MyEnum
→double
→MyClass
.
Both are considered to be “Conversions” but
the latter contains a user-defined conversion (the double
→MyClass
) which is defined to be
worse than the standard conversion sequence of the former. This makes F1 better than F2 for this parameter.
Given this contradiction in the parameter conversions, neither F1 nor F2 can be said to be better than the other, and so the call is ambiguous and the program is ill-formed.
So what does GCC’s error message mean? Looking at it again:
ISO C++ says that these are ambiguous, even though the
worst conversion for the first is better than the worst
conversion for the second
The “worst conversion for the first” is the worst conversion for F1. Both conversions needed to call
F1 are considered to be of equal weighting: the char
→int
and MyEnum
→int
are both standard conversions.
The “worst conversion for the second” is the user-defined conversion MyEnum
→double
→MyClass
.
Comparing these two conversions is like saying “if I had to pick one, which one is the least worst?” The user-defined conversion of F2 is a worse conversion than F1’s standard conversion. This makes F1 seem a better choice — its worst-case choice of parameter conversion is better than F2’s worst-case.
Given this, you might imagine that choosing F1 would be the right thing
to do — the operator(int,int)
that Microsoft Visual C++ picks.
I’m not quite sure why the ISO committee chose to leave this case ambiguous. Earlier versions of GCC don’t seem to have been quite so pernickity: it would appear that this strict behaviour was only implemented in GCC 3.3 and above.
Getting back to the original problem, in this case (and indeed in Rich’s original case)
solving the problem is quite easy. By making the double
constructor in MyClass
an explicit
one, you prevent it from being used as during implicit conversion. This leaves
only one way to compare the char
and MyEnum
, which is using the built-in operator==(int,int)
.
This is a minimal form of the original code Rich had.
:::cpp enum MyEnum { A, B, C };
class MyClass { public: MyClass(double) {} };
bool operator == (char, MyClass) { return false; }
int main() { char aChar = 0; MyEnum anEnum = A; return aChar == anEnum; // What happens here? } ↩
The references cited here are to sections within the C++ standard. ↩
Matt Godbolt is a C++ developer living in Chicago. Follow him on Mastodon or Bluesky.