Argument conversion ranking takes precedence over return type conversion ranking in overload resolution for user-defined conversions
All standard references below refers to N4659: March 2017 post-Kona working draft/C++17 DIS.
Preparations
To avoid having to deal with segfaults, auto
type deduction and consideration of explicit
marked conversion functions, consider the following simplified variation of your example, which shows the same behaviour:
#include <iostream>
struct Foo {
int bar{42};
operator int&() {
std::cout << __PRETTY_FUNCTION__;
return bar;
}
operator bool() const {
std::cout << __PRETTY_FUNCTION__;
return true;
}
};
int main() {
Foo f {};
// (A):
bool a = f; // Foo::operator int&()
}
TLDR
Why does bool(f)
call bool(f.operator int&())
and not f.operator bool()
as was intended?
The viable candidate functions to the initialization conversion sequence are
operator int&(Foo&)
operator bool(const Foo&)
and [over.match.best]/1.3, ranking on function arguments, takes precedence over [over.match.best]/1.4, ranking on conversion from return type, meaning operator int&(Foo&)
will be chosen as it is a non-ambiguous perfect match, for an argument of type Foo&
, by the [over.match.best]/1.3 rule, whereas operator bool(const Foo&)
is not.
In this regard, as we are relying on [over.match.best]/1.3, it is no different than simply overloading solely on const
-qualification:
#include <iostream>
struct Foo {};
void f(Foo&) { std::cout << __PRETTY_FUNCTION__ << "\n"; }
void f(const Foo&) { std::cout << __PRETTY_FUNCTION__ << "\n"; }
int main() {
Foo f1 {};
const Foo f2{};
f(f1); // void f(Foo&)
f(f2); // void f(const Foo&)
}
Is there a way to make bool(f)
call f.operator bool()
as intended without marking operator int&
as explicit?
As per above, if you provide matching cv-qualifiers for the member function overloads, there can no longer be any differentiation on the implicit object argument as per [over.match.best]/1.3, and the bool
conversion function will be chosen as most viable as per [over.match.best]/1.4. Note that by marking the int&
conversion functions as explicit
, it will no longer be a viable candidate, and the choice for the bool
conversion function will not be due to it being the most viable overload, but by it being the only viable overload.
Details
The expression at (A) is initialization, with semantics governed specifically by [dcl.init]/17.7 [extract, emphasis mine]:
[dcl.init]/17 The semantics of initializers are as follows. The destination
type is the type of the object or reference being initialized and the
source type is the type of the initializer expression.
- [...]
- /17.7 Otherwise, if the source type is a (possibly cv-qualified) class type, conversion functions are considered. The applicable
conversion functions are enumerated ([over.match.conv]), and the best
one is chosen through overload resolution. The user-defined conversion
so selected is called to convert the initializer expression into the
object being initialized. [...]
where [over.match.conv]/1 describes which conversion functions that are considered candidate functions in overload resolution:
[over.match.conv]/1 Under the conditions specified in [dcl.init], as part of an initialization of an object of non-class type, a conversion function can be invoked to convert an initializer expression of class type to the type of the object being initialized. Overload resolution is used to select the conversion function to be invoked. Assuming that “cv1 T
” is the type of the object being initialized, and “cv S
” is the type of the initializer expression, with S
a class type, the candidate functions are selected as follows:
- /1.1 The conversion functions of
S
and its base classes are considered. Those non-explicit conversion functions that are not
hidden within S
and yield type T
or a type that can be converted
to type T
via a standard conversion sequence are candidate
functions. [...] Conversion functions that return “reference to cv2
X
” return lvalues or xvalues, depending on the type of reference, of
type “cv2 X
” and are therefore considered to yield X
for this
process of selecting candidate functions.
In this example, cv T
, the type of the object being initialized, is bool
, and thus both used-defined conversion functions are viable candidates, as one directly yields type bool
and the other yields a type that can be converted to type bool
via a standard conversion sequence (int
to bool
); as per [conv.bool]:
A prvalue of arithmetic, unscoped enumeration, pointer, or pointer to member type can be converted to a prvalue of type bool. [...]
Moreover, the type of the initializer expression is Foo
, and [over.match.funcs]/4 governs that the cv-qualification of the type of the implicit object parameter for user-defined conversion functions is that of the cv-qualification of the respective function:
For non-static member functions, the type of the implicit object
parameter is
- “lvalue reference to cv
X
” for functions declared without a ref-qualifier or with the & ref-qualifier
- [...]
where X
is the class of which the function is a member and cv is
the cv-qualification on the member function declaration. [...] For
conversion functions, the function is considered to be a member of the
class of the implied object argument for the purpose of defining the
type of the implicit object parameter. [...]
Thus, w.r.t. overload resolution we may summarize as follows:
// Target type: bool
// Source type: Foo
// Viable candidate functions:
operator int&(Foo&)
operator bool(const Foo&)
where we have added the implied object parameter as explicit function parameter, without loss of generality (as per [over.match.funcs]/5), when continuing for how overload resolution picks the best viable candidate.
Now, [over.ics.user], particularly [over.ics.user]/2 summarizes this:
[over.ics.user]/2 [...] Since an implicit conversion sequence is an initialization, the special rules for initialization by user-defined conversion apply when selecting the best user-defined conversion for a user-defined conversion sequence (see [over.match.best] and [over.best.ics]).
particularly that the rules for selecting the best viable candidate is governed by [over.match.best], particularly [over.match.best]/1:
[over.match.best]/1 [...] Given these definitions, 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
- /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 [dcl.init], [over.match.conv], and [over.match.ref])
and the standard conversion sequence from the return type of
F1
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 of F2
to the destination
type
The key here is that [over.match.best]/1.4, regarding conversion on the return type of the candidate (to the target type) only applies if the overloads cannot be disambiguated by means of [over.match.best]/1.3. In our example, however, recall that the viable candidate functions are:
operator int&(Foo&)
operator bool(const Foo&)
As per [over.ics.rank]/3.2, particularly [over.ics.rank]/3.2.6:
[over.ics.rank]/3 Two implicit conversion sequences of the same form are indistinguishable conversion sequences unless one of the
following rules applies:
- [...]
- /3.2 Standard conversion sequence
S1
is a better conversion sequence than standard conversion sequence S2
if
- [...]
- /3.2.6
S1
and S2
are reference bindings ([dcl.init.ref]), and the types to which the references refer are the same type
except for top-level cv-qualifiers, and the type to which the
reference initialized by S2
refers is more cv-qualified than the
type to which the reference initialized by S1
refers.
meaning, that for an argument of type Foo&
, operator int&(Foo&)
will be a better (/1.3: exact) match, whereas for e.g. an argument of type const Foo&
, operator bool(const Foo&)
will the only match (Foo&
will not be viable).