RustyX RustyX - 2 months ago 12
C++ Question

Precedence of const member function over return value type match

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()

M.M M.M
Answer

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.

Comments