由不同编译器调用的不同强制转换运算符

考虑下面这个简短的 C + + 程序:

#include <iostream>


class B {
public:
operator bool() const {
return false;
}
};


class B2 : public B {
public:
operator int() {
return 5;
}
};


int main() {
B2 b;
std::cout << std::boolalpha << (bool)b << std::endl;
}

如果我在不同的编译器上编译它,我会得到不同的结果。在 Clang 3.4和 GCC 4.4.7中,它打印 true,而 Visual Studio 2013打印 false,这意味着它们在 (bool)b中调用不同的强制转换操作符。根据标准,哪种行为是正确的?

根据我的理解,operator bool()不需要转换,而 operator int()需要 intbool的转换,所以编译器应该选择第一个。const是否对此进行了处理,常量转换是否被编译器认为更“昂贵”?

如果删除 const,所有编译器都会同样地生成 false作为输出。 另一方面,如果我将这两个类组合在一起(两个操作符将在同一个类中) ,所有三个编译器将生成 true输出。

2598 次浏览

C + + bool 类型有两个值—— true 和 false,对应的值为1和0。如果在 B2类中添加一个显式调用基类(B)的 bool 运算符的 bool 运算符,则输出结果为 false,这样就可以避免内在的混淆。 这是我修改过的程序。然后运算符 bool 表示运算符 bool,而不是运算符 int。

#include <iostream>


class B {
public:
operator bool() const {
return false;
}
};


class B2 : public B {
public:
operator int() {
return 5;
}
operator bool() {
return B::operator bool();
}
};


int main() {
B2 b;
std::cout << std::boolalpha << (bool)b << std::endl;
}

在你的例子中,(bool) b 试图为 B2调用 bool 运算符,B2继承了 bool 运算符,而 int 运算符,根据支配规则,int 运算符被调用,B2中继承了 bool 运算符。然而,通过在 B2类本身显式地拥有一个 bool 运算符,问题得到了解决。

标准规定:

派生类中的转换函数不会隐藏基类中的转换函数,除非这两个函数转换为同一类型。

12.3[ class.conv ]

这意味着 operator bool不被 operator int隐藏。

标准规定:

在重载解析期间,隐含的对象参数与其他参数没有区别。

13.3.3.1[ over.match. funs ]

本例中的“隐含对象参数”是 b,它的类型是 B2 &operator bool需要 const B2 &,因此编译器必须将 const 添加到 b才能调用 operator bool。在其他条件相同的情况下,这使得 operator int更加匹配。

该标准规定,如果下列情况下,static_cast(C 风格的强制转换在此实例中执行)可以转换为 T类型(在本例中为 int) :

声明 T t(e);是格式良好的,因为一些人发明了临时变量 t

5.2.9[ expr.static.cast ]

因此,int可以转化为 bool,而 bool也可以同样转化为 bool

标准规定:

考虑了 S及其基类的转换函数。那些没有隐藏在 S和产量类型 T 或者可以通过标准转换序列转换为 T类型的类型中的非显式转换函数是候选函数。

13.3.1.5[ over.match. conv ]

所以重载集由 operator intoperator bool组成。在其他条件相同的情况下,operator int是一个更好的匹配(因为不需要添加常量)。因此应该选择 operator int

请注意(也许违背直觉) ,一旦这些操作符被添加到重载集(如上所述) ,标准不会考虑返回类型(即这些操作符转换的类型) ,前提是其中一个操作符的参数的转换序列优于另一个操作符的参数的转换序列(由于常量性,在本例中就是这种情况)。

标准规定:

鉴于这些定义,如果对于所有参数 i,ICSi (F1)不是比 ICSi (F2)更差的转换序列,则可行函数 F1被定义为比另一个可行函数 F2更好的函数,然后

  • 对于某些参数 j,ICSj (F1)是比 ICSj (F2)更好的转换序列,或者,如果不是,
  • 上下文是通过用户定义的转换进行初始化,从 F1的返回类型到目标类型的标准转换序列(即,被初始化的实体的类型)比从 F2的返回类型到目标类型的标准转换序列要好。

13.3.3[ over.match. best ]

在这种情况下,只有一个参数(隐式 this参数)。B2 & = > B2 &(调用 operator int)的转换序列优于 B2 & = > const B2 &(调用 operator bool) ,因此 operator int是从过载集中选择的,而不考虑它实际上不直接转换为 bool的事实。

很短

转换函数 operator int()是通过在 operator bool() const上发出叮当声来选择的,因为 b不符合常数,而 bool 的转换运算符符合常数。

简短的推理是,当将 b转换为 bool时,用于重载解析的候选函数(隐式对象参数就位)是

operator bool (B2 const &);
operator int (B2 &);

因为 b不符合常数,所以第二个匹配更好。

如果两个函数共享相同的限定条件(无论是否都是 const) ,则选择 operator bool,因为它提供直接转换。

通过强制符号转换,逐步分析

如果我们同意使用 bbool的转换所产生的值调用布尔型 ostream 插入器(std: : basic _ ostream: : Operator < < (bool val) as per [ ostream. insert ters.)) ,我们可以深入研究这种转换。

1. 演员表情

布鲁的演员们

(bool)b

评估为

static_cast<bool>(b)

按照 C + + 11,5.4/4[ expr.cast ],因为 const_cast不适用(这里不添加或删除 const)。

这个静态转换是允许每个 C + + 11,5.2.9/4[ expr.static.cast ],如果 bool t(b);为一个发明的变量 t 是良好的形式。 根据 C + + 11,8.5/15[ dcl.init ],这样的语句称为直接初始化。

2. 直接初始化 bool t(b);

最不被提及的标准段落的 16条款规定(强调我的) :

初始化器的语义如下。目标类型是要初始化的对象或引用的类型,源类型是初始化器表达式的类型。

[...]

[ ... ]如果 来源类型是一个(可能 cv 合格的) 类类型,转换函数被考虑。

列举了适用的转换函数,并通过重载分解选择了最佳的转换函数。

2.1有哪些转换功能?

可用的转换函数是 operator int ()operator bool() const,因为 C + + 11,12.3/5[ class.conv ]告诉我们:

派生类中的转换函数不会隐藏基类中的转换函数,除非这两个函数转换为同一类型。

C + + 11,13.3.1.5/1[ over.match. conv ]指出:

考虑了 S 及其基类的转换函数。

其中 S 是将从。

2.2哪些转换函数适用?

C + + 11,13.3.1.5/1[ over.match. conv ] (强调我的) :

1 [ ... ]假设“ cv1T”是被初始化对象的类型,“ cvS”是初始化器表达式的类型,S 是类类型,候选函数的选择如下: 考虑了 S 及其基类的转换函数。那些不隐藏在 S 和屈服类型 T 或者可以通过标准转换序列转换为 T 型的类型中的非显式转换函数是候选函数。

因此,operator bool () const是适用的,因为它不隐藏在 B2中,并且产生一个 bool

最后一个标准报价中强调的部分与使用 operator int ()的转换相关,因为 int是一种可以通过标准转换序列转换为 bool 的类型。 从 intbool的转换甚至不是一个序列,而是一个普通的直接转换,每 < em > C + + 11,4.12/1[ conv.bool ]允许这种转换

可以将算术、无作用域枚举、指针或成员类型指针的预值转换为 bool 类型的预值。零值、空指针值或空成员指针值转换为 false; 其他任何值都转换为 true。

这意味着 operator int ()也是适用的。

2.3选择哪个转换函数?

选择合适的转换函数是通过过载分辨率(C + + 11,13.3.1.5/1[ over.match. conv ])执行的:

重载解析用于选择要调用的转换函数。

当涉及到类成员函数的重载解析时,有一个特殊的“怪癖”: 隐式对象参数。

根据 C + + 11,13.3,

[ ... ]静态和非静态成员函数都有一个隐式的对象参数[ ... ]

其中,非静态成员函数的这个参数的类型(根据子句4)是:

  • 对于没有使用 ref 限定符或 & ref 限定符声明的函数,“ lvalue reference to cv X”

  • 对于用 & & ref 限定符声明的函数,“ cv X 的 rvalue 引用”

其中 X 是函数是其成员的类和 Cv 是成员函数声明上的 cv 限定符。

这意味着(根据 C + + 11,13.3.1.5/2[ over.match. conv ]) ,在通过转换函数进行初始化时,

[ t ]参数列表有一个参数,即初始化器表达式。[注意: 此参数将与转换函数的隐式对象参数进行比较。ー尾注]

重载解决方案的候选函数是:

operator bool (B2 const &);
operator int (B2 &);

显然,如果使用类型为 B2的非常数对象请求转换,则 operator int ()是一个更好的匹配,因为 operator bool ()需要限定转换。

如果两个转换函数共享相同的常量限定,那么这些函数的重载分辨率就不再起作用了。 在这种情况下,转换(序列)排名就到位了。

3. 当两个转换函数共享相同的常量限定时,为什么选择 operator bool ()

B2bool的转换是用户定义的转换序列(C + + 11,13.3.3.1.2/1[ over.ics.user ])

用户定义转换序列由初始标准转换序列、用户定义转换、第二标准转换序列组成。

[ ... ]如果用户定义的转换是由转换函数指定的,则初始标准转换序列将源类型转换为转换函数的隐式对象参数。

C + + 11,13.3.3.2/3[ over.ics.rank ]

[ ... ]定义了隐式转换序列的偏序,该偏序基于更好的转换序列和更好的转换关系。

[ ... ]用户自定义转换序列 U1比另一个用户自定义转换序列 U2具有更好的转换序列,如果它们包含相同的用户自定义转换函数或构造函数或聚合初始化,并且 U1的第二个标准转换序列优于 U2的第二个标准转换序列。

第二个标准转换是 operator bool()boolbool(身份转换) ,而第二个标准转换是 operator int ()intbool,这是一个布尔转换。

因此,如果两个转换函数具有相同的常量限定,则使用 operator bool ()的转换序列更好。

以前的一些答案,已经提供了很多信息。

我的贡献是,“强制转换操作”的编译类似于“重载操作”,我建议为每个操作创建一个带唯一标识符的函数,然后用所需的操作符或强制转换替换它。

#include <iostream>


class B {
public:
bool ToBool() const {
return false;
}
};


class B2 : public B {
public:
int ToInt() {
return 5;
}
};


int main() {
B2 b;
std::cout << std::boolalpha << b.ToBool() << std::endl;
}

然后,应用操作符或强制转换。

#include <iostream>


class B {
public:
operator bool() {
return false;
}
};


class B2 : public B {
public:
operator int() {
return 5;
}
};


int main() {
B2 b;
std::cout << std::boolalpha << (bool)b << std::endl;
}

只有我的两分钱。