Why does bool(val) prefer double implicit conversions over val.operator bool()?
Asked Answered
S

1

7

The piece of code below dereferences a nullptr.

struct Foo {
    int *bar;
    operator int&() {
        return *bar;
    }
    explicit operator bool() const {
        return bar;
    }
};
int main() {
    Foo f {nullptr};
    auto _ = bool(f); 
}
  1. Why does bool(f) call bool(f.operator int&()) and not f.operator bool() as was intended?
  2. Is there a way to make bool(f) call f.operator bool() as intended without marking operator int& as explicit?
Stempson answered 14/11, 2020 at 21:15 Comment(0)
M
6

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 Xand 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).

Merkle answered 16/11, 2020 at 11:19 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.