为什么 bool(val) 比 val.operator bool() 更喜欢双重隐式转换?

Jak*_*idt 7 c++ operator-overloading type-conversion language-lawyer implicit-conversion

下面的一段代码取消引用 a nullptr

struct Foo {
    int *bar;
    operator int&() {
        return *bar;
    }
    explicit operator bool() const {
        return bar;
    }
};
int main() {
    Foo f {nullptr};
    auto _ = bool(f); 
}
Run Code Online (Sandbox Code Playgroud)
  1. 为什么bool(f)调用bool(f.operator int&())而不是f.operator bool()预期的那样?
  2. 有没有办法在标记为的情况下按预期bool(f)拨打电话?f.operator bool()operator int&explicit

dfr*_*fri 6

在用户定义的转换的重载解析中,参数转换排名优先于返回类型转换排名

以下所有标准参考均指N4659:2017 年 3 月后 Kona 工作草案/C++17 DIS


准备工作

为了避免处理段错误、auto类型推导和explicit标记转换函数的考虑,请考虑以下示例的简化变体,它显示了相同的行为:

#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&()
}
Run Code Online (Sandbox Code Playgroud)

TLDR

为什么bool(f)调用bool(f.operator int&())而不是f.operator bool()预期的那样?

初始化转换序列的可行候选函数是

operator int&(Foo&)     
operator bool(const Foo&) 
Run Code Online (Sandbox Code Playgroud)

[over.match.best]/1.3,在函数参数上排名,优先于[over.match.best]/1.4,在从返回类型转换上排名,意思是operator int&(Foo&)将被选择,因为它是一个非歧义的完美匹配,对于 type 的参数Foo&,根据 [over.match.best]/1.3 规则,而operator bool(const Foo&)不是。

在这方面,由于我们依赖于 [over.match.best]/1.3,它与仅在 -qualification 上简单重载没有什么不同const

#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&)
}
Run Code Online (Sandbox Code Playgroud)

有没有一种方法可以在不标记为显式的情况下按预期进行bool(f)调用?f.operator bool() operator int&

如上所述,如果您为成员函数重载提供匹配的 cv 限定符,则根据 [over.match.best]/1.3,隐式对象参数不再有任何区别,并且bool转换函数将被选择为最根据 [over.match.best]/1.4 可行。请注意,通过将int&转换函数标记为explicit,它将不再是可行的候选者,并且bool转换函数的选择不是因为它是可行的重载,而是因为它是唯一可行的重载。


细节

(A) 处的表达式是初始化,语义由[dcl.init]/17.7 [extract,重点是我的]专门管理:

[dcl.init]/17初始化器的语义如下。目标类型是被初始化的对象或引用的类型,源类型是初始化表达式的类型。

  • [...]
  • /17.7否则,如果源类型是(可能是 cv 限定的)类类型,则考虑转换函数。列举出适用的转换函数([over.match.conv]),通过重载决议选择最好的一个。调用如此选择的用户定义转换将初始化表达式转换为正在初始化的对象。[...]

其中[over.match.conv]/1描述了哪些转换函数被视为重载解析中的候选函数

[over.match.conv]/1在[dcl.init]中指定的条件下,作为非类类型对象初始化的一部分,可以调用转换函数将类类型的初始化表达式转换为被初始化的对象的类型。重载解析用于选择要调用的转换函数。假设“cv1 T”是被初始化对象的类型,“cv S”是初始化表达式的类型,具有S类类型,候选函数选择如下:

  • /1.1S考虑其基类的转换函数。那些没有隐藏在S和 yield 类型T可以T通过标准转换序列转换为类型的类型的非显式转换函数是候选函数。[...] 返回“对 cv2 的引用”的转换函数会返回左值 X或 x 值,具体取决于引用的类型,类型为“cv2 X,因此被视为在X选择候选函数的过程中产生收益。

在这个例子中,cv T,被初始化的对象的类型是bool,因此两个使用定义的转换函数都是可行的候选者,因为一个直接产生类型bool,另一个产生可以bool通过标准转换序列转换为类型的类型(intbool); 根据[conv.bool]

算术、无作用域枚举、指针或指向成员类型的指针的纯右值可以转换为 bool 类型的纯右值。[...]

此外,初始化表达式的类型是Foo,并且[over.match.funcs]/4控制用户定义转换函数的隐式对象参数的类型的 cv 限定是各自的 cv 限定功能:

对于非静态成员函数,隐式对象参数的类型为

  • “对 cv 的左值引用X”,用于未使用 ref 限定符或使用 & ref 限定符声明的函数
  • [...]

其中X是函数所属的类,cv成员函数声明中的 cv 限定。[...] 对于转换函数,为了定义隐式对象参数的类型,函数被认为是隐式对象参数的类的成员。[...]

因此,wrt 重载解析我们可以总结如下:

// Target type: bool
// Source type: Foo

// Viable candidate functions:
operator int&(Foo&)     
operator bool(const Foo&) 
Run Code Online (Sandbox Code Playgroud)

在继续讨论重载决议如何选择最佳可行候选者时,我们在不失一般性的情况下添加了隐含对象参数作为显式函数参数(根据[over.match.funcs]/5)。

现在,[over.ics.user],特别是[over.ics.user]/2总结了这一点:

[over.ics.user]/2 [...] 由于隐式转换序列是一个初始化,当为用户定义的转换序列选择最佳用户定义的转换时,应用用户定义的转换初始化特殊规则(参见 [over.match.best] 和 [over.best.ics])。

特别是选择最佳可行候选人的规则由[over.match.best] 管理,特别是[over.match.best]/1

[over.match.best]/1 [...] 鉴于这些定义,一个可行函数F1被定义为比另一个可行函数更好的函数,F2如果对于所有参数iICSi(F1)不是比 更差的转换序列ICSi(F2),然后

  • /1.3对于某些参数jICSj(F1)是比 更好的转换序列ICSj(F2),或者,如果不是
  • /1.4上下文是由用户定义的转换(见[dcl.init]、[over.match.conv]和[over.match.ref])和从返回类型F1的标准转换序列到目的地的初始化type(即被初始化的实体的类型)是一个比标准的从返回类型的转换序列F2到目标类型的更好的转换序列

这里的关键是 [over.match.best]/1.4,关于候选返回类型(到目标类型)的转换仅适用于无法通过 [over.match.best]/1.3 消除重载的歧义. 然而,在我们的例子中,回想一下可行的候选函数是:

operator int&(Foo&)     
operator bool(const Foo&)
Run Code Online (Sandbox Code Playgroud)

根据[over.ics.rank]/3.2,特别是[over.ics.rank]/3.2.6

[over.ics.rank]/3两个相同形式的隐式转换序列是不可区分的转换序列,除非以下规则之一适用:

  • [...]
  • /3.2标准转换序列S1是比标准转换序列更好的转换序列,S2如果
    • [...]
    • /3.2.6 S1S2是参考绑定([dcl.init.ref]),以及其中的参考文献是指如下类型 除了顶层cv修饰符相同类型,并且其通过初始化的基准的类型S2是指比引用初始化的引用类型更多的 cv 限定S1

这意味着,对于类型的参数Foo&operator int&(Foo&)会是一个更好的(/1.3:准确)的比赛,而对于如类型的参数const Foo&operator bool(const Foo&)将只匹配(Foo&不会是可行的)。