Consider the following piece of code:
#include <utility>
#include <iostream>
class Class
{
int x;
public:
explicit operator int()
{
std::cout << __func__ << "\n";
return 1;
}
explicit operator int&()
{
std::cout << __func__ << "\n";
return x;
}
explicit operator int&&()
{
std::cout << __func__ << "\n";
return std::move(x);
}
};
void foo(int)
{
return;
}
void bar(int&)
{
return;
}
void baz(int&&)
{
return;
}
Class test()
{
return {};
}
int main()
{
Class obj;
foo(static_cast<int>(obj)); // <---- THE ERROR IS ON THIS LINE
bar(static_cast<int&>(obj));
baz(static_cast<int&&>(std::move(obj)));
}
When I run this code, I get this error:
<source>: In function 'int main()':
<source>:51:26: error: conversion from 'Class' to 'int' is ambiguous
51 | foo(static_cast<int>(obj));
| ^~~
<source>:51:26: note: there are 3 candidates
<source>:9:14: note: candidate 1: 'Class::operator int()'
9 | explicit operator int()
| ^~~~~~~~
<source>:15:14: note: candidate 2: 'Class::operator int&()'
15 | explicit operator int&()
| ^~~~~~~~
<source>:21:14: note: candidate 3: 'Class::operator int&&()'
21 | explicit operator int&&()
| ^~~~~~~~
Compiler returned: 1
I don't understand why I'm getting this error. Since I'm explicitly casting obj
to int
, and all the three conversion operators are marked as explicit
, why is the compiler getting confused here?
I'm using gcc trunk on compiler explorer and compiling with -Ofast -std=c++23 -Wconversion -Wall -Wextra -Wpedantic -fanalyzer
.
Here's the link to Godbolt.
Interestingly, compiling with clang
results in even more errors. Here's the link to Godbolt.
<source>:51:9: error: ambiguous conversion for static_cast from 'Class' to 'int'
51 | foo(static_cast<int>(obj));
| ^~~~~~~~~~~~~~~~~~~~~
<source>:9:14: note: candidate function
9 | explicit operator int()
| ^
<source>:15:14: note: candidate function
15 | explicit operator int&()
| ^
<source>:21:14: note: candidate function
21 | explicit operator int&&()
| ^
<source>:53:9: error: reference initialization of type 'int &&' with initializer of type 'typename std::remove_reference<Class &>::type' (aka 'Class') is ambiguous
53 | baz(static_cast<int&&>(std::move(obj)));
| ^ ~~~~~~~~~~~~~~
<source>:9:14: note: candidate function
9 | explicit operator int()
| ^
<source>:21:14: note: candidate function
21 | explicit operator int&&()
| ^
2 errors generated.
Do I have undefined behavior in my code?
The expression static_cast<int>(obj)
has the same semantics as a direct-initialization
int x(obj);
The rules of the language compute a set of "permissible types" for explicit conversion functions, and then the conversion functions that satisfy this criterion are submitted to overload resolution. According to [over.match.conv]/1.1,
[...] For direct-initialization, the permissible types for explicit conversion functions are those that can be converted to type
T
with a (possibly trivial) qualification conversion ([conv.qual]); otherwise there are none.
T
in this case is int
, so the permissible types are cv int
only. According to [over.match.funcs.general]/7:
[...] If initializing an object, for any permissible type cv
U
, any cv2U
, cv2U&
, or cv2U&&
is also a permissible type.
So, in fact, the permissible types are cv int
, cv int&
, and cv int&&
(note that the cv pertains to the int
, not to the reference). That makes operator int
, operator int&
, and operator int&&
all candidates.
Why is it like that? Because, when you call an operator int
, operator int&
, or operator int&&
, in all cases the result of that call has type int
; but the first one gives you a prvalue, the second an lvalue, and the third an xvalue. But since they all have type int
, they're all equally good when the destination type is int
.
Note that the situation is different when you are initializing a reference. Initializing int&
prefers a conversion function with return type int&
. Initializing int&&
prefers a conversion function with return type int
or int&&
(we do not discriminate between prvalues and xvalues), but can fall back on calling a conversion function with return type int&
, making a copy, and then binding to that copy.