I have something along the lines of:
#include <iostream>
class Foo;
struct Test
{
template <typename T>
operator T() const // <----- This const is what puzzles me
{
std::cout << "Template conversion" << std::endl;
return T{};
}
operator Foo*()
{
std::cout << "Pointer conversion" << std::endl;
return nullptr;
}
};
int main()
{
Test t;
if (t)
{
std::cout << "ahoy" << std::endl;
}
bool b = (bool)t;
Foo* f = (Foo*)t;
}
It builds fine, but when I run it, while I would expect to get
$> ./a.out
Template conversion
Template conversion
Pointer conversion
I instead get
$> ./a.out
Pointer conversion
Pointer conversion
Pointer conversion
If I remove the const, or make the Test instance const, then everything works as expected. More precisely, the overload selection seems to make sense strictly when both operators have the same const qualification.
13.3.3.1.2 point of the standard makes me think I should get an identity conversion, converting to a bool, using the template conversion operator instantiation with a T
= bool
, though there is obviously a subtlety hiding somewhere. Could someone enlighten me as to what rule comes into play here?
The relevant rules are defined in [over.match.best]:
Given these definitions, a viable function
F1
is defined to be a better function than another viable functionF2
if for all arguments i, ICSi(F1
) is not a worse conversion sequence than ICSi(F2
), and then
(1.3) — for some argument j, ICSj(F1
) is a better conversion sequence than ICSj(F2
), or, if not that,
(1.4) — the context is an initialization by user-defined conversion (see 8.5, 13.3.1.5, and 13.3.1.6) and the standard conversion sequence from the return type ofF1
to the destination type (i.e., the type of the entity being initialized) is a better conversion sequence than the standard conversion sequence from the return type ofF2
to the destination type.
Let's just look at the first bool
case. We have two viable candidates:
Test::operator T<bool>() const;
Test::operator Foo*();
Both are called with a non-const
Test
. For the 2nd overload, no conversions are necessary - the conversion sequence is simply Exact Match. However, for the first overload, the implicit this
argument needs to undergo a qualification conversion from Test
to const Test
. Thus, the second overload is preferred - we do not get to the second step which discusses return type.
If we dropped the const
however, the viable candidates become:
Test::operator T<bool>();
Test::operator Foo*();
Here, both candidates are equally viable with identical conversion sequences, but the bool
template is preferred since the conversion sequence from the return type bool
to bool
(Identity - the highest rank) is a better conversion sequence than from Foo*
to bool
(Boolean Conversion - the lowest).
When comparing conversion sequences, the conversions on the parameters are considered before the conversion of the result type. The implicit object parameter (the this
pointer) is considered as a parameter, and a qualification conversion (Foo -> Foo const
) is worse than the identity conversion on the implicit object parameter. From [over.match.best]:
1 - [...] a viable function F1 is defined to be a better function than another viable function F2 if for all arguments i, ICSi(F1) is not a worse conversion sequence than ICSi(F2), and then
— for some argument j, ICSj(F1) is a better conversion sequence than ICSj(F2), or, if not that, [...]
So a non-const
-qualified member conversion operator will always be better than a const
-qualified one, even if the result conversion is exact on the latter.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With