In Y::test1()
a non-const X::operator void*()
takes precedence over a seemingly better match, X::operator bool() const
- Why is that? And where is this phenomenon described in the standard?
#include <iostream>
struct X {
operator void*() { std::cout << " operator void*()\n"; return nullptr; }
operator bool() const { std::cout << " operator bool()\n"; return true; }
};
struct Y {
X x;
bool test1() { std::cout << "test1()\n"; return x; }
bool test2() const { std::cout << "test2()\n"; return x; }
};
int main() {
Y y;
y.test1();
y.test2();
}
Output:
test1()
operator void*()
test2()
operator bool()
If the thing you are returning by reference is logically part of your this object, independent of whether it is physically embedded within your this object, then a const method needs to return by const reference or by value, but not by non-const reference.
The const qualifier at the end of a member function declaration indicates that the function can be called on objects which are themselves const. const member functions promise not to change the state of any non-mutable data members.
The const keyword can be used as a qualifier when declaring objects, types, or member functions. When qualifying an object, using const means that the object cannot be the target of an assignment, and you cannot call any of its non-const member functions.
To make a member function constant, the keyword “const” is appended to the function prototype and also to the function definition header. Like member functions and member function arguments, the objects of a class can also be declared as const.
First of all: when converting the expression in a return
statement to the return type of the function, the rules are the same as for initialization (see [conv]/2.4 and [conv]/3).
So we could examime the behaviour of the code using this example instead (with the same X
as you have, but without Y
):
X test1;
bool b1 = test1;
X const test2;
bool b2 = test2;
(in the call y.test2()
, the type of this->x
is X const
, that's what it means to have a const member function). It would also be the same if we cast to bool
instead of writing an initialization statement.
The part of the Standard dealing with overload resolution in this situation is [over.match.conv], here is the C++14 text (with some elision for brevity):
13.3.1.5 Initialization by conversion function [over.match.conv]
1 Under the conditions specified in 8.5, as part of an initialization of an object of nonclass type, a conversion function can be invoked to convert an initializer expression of class type to the type of the object being initialized. [...]
2 The argument list has one argument, which is the initializer expression. [Note: This argument will be compared against the implicit object parameter of the conversion functions. —end note ]
The test2
case is straightforward - a non-const member function cannot be called on a const object, so the operator void*
is never considered, there is only one candidate and no need for overload resolution. operator bool()
is called.
So for the rest of this post I will just talk about the test1
case. The part I elided in the above quote covers that both operator bool
and operator void*()
are candidate functions for the test1
case.
It is important to note that overload resolution selects amongst these two candidate functions, and it is not a case of considering two implicit conversion sequences, each containing a user-defined conversion. To back this up, look at the first sentence of [over.best.ics]:
An implicit conversion sequence is a sequence of conversions used to convert an argument in a function call to the type of the corresponding parameter of the function being called.
We are not converting an argument in a function call here. The rules about implicit conversion sequences come into play when we are ranking candidate functions: the rules are applied to each argument of each candidate function, as we shall see in a moment.
So now we look to the rules for best viable function to determine which of these two candidate functions is selected. I'll skip [over.match.viable], which clarifies that both of those candidates are viable, and onto [over.match.best].
The key part of that section is [over.match.best]/1.2:
let ICSi(F) denote the implicit conversion sequence that converts the i-th argument in the list to the type of the i-th parameter of viable function F. 13.3.3.1 defines the implicit conversion sequences and 13.3.3.2 defines what it means for one implicit conversion sequence to be a better conversion sequence or worse conversion sequence than another.
Here, i == 1
, there is only one argument test1
as explained by [over.match.conv]/2 above -- the argument is the initializer expression test1
; the "parameter of the viable function" is the implicit object parameter of the member functions of X
.
Now the implicit conversion sequence rules apply:
operator void*()
requires no conversion - the argument is X
and the parameter is X&
operator bool() const
requires a qualification conversion - the argument is X
and the parameter is X const&
No conversion is better than qualification conversion ([over.ics.rank]/3.1.1). So ICS1(operator void*()
) is a better conversion sequence than ICS1(operator bool() const
); so at this point operator void*()
wins ([over.match.best]/1.3).
The subsequent paragraph [over.match.best]/1.4 explains what would have happened if neither of those two sequences was better: we would only then go on to compare the standard conversion sequences from the return type of the candidate function onto the type being initialized.
You can explore this case by changing the X
member to operator void*() const
. Now the two ICS1 sequences are indistinguishable as of /1.3, so we go onto /1.4 at which point operator bool() const
wins because its conversion to bool
is the identity, whereas operator void*() const
still requires a boolean conversion.
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