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


80

考虑以下简短的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()需要进行inttobool转换,因此编译器应选择第一个。是否const做的东西,是由编译器认为更“贵”常量转换?

如果删除const,则所有编译器均会产生false输出。另一方面,如果我将两个类组合在一起(两个运算符都在同一个类中),则所有三个编译器都会产生true输出。


3
据我了解,B2有2个运算符:B :: operator bool()和B2 :: operator int()。这两个运算符都不是const运算符,并且const和非const版本之间存在差异。您可以将bool的const版本添加到B中,将int的const版本添加到B2中,然后查看更改。
Tanuki 2014年

1
const是这里的关键。两种运算符都相等或不相等,它们的行为均符合预期,即bool版本“ wins”。使用不同的constness,非const版本总是在每个平台上“获胜”。由于派生类和运算符的常数不同,VS似乎有一个错误,因为它的行为不同于其他两个编译器。
2014年

不同的默认构造函数可以负责不同的行为吗?
ldgorman 2014年

16
EDG也可以打印true。如果GCC,Clang和EDG都同意而MSVC不同意,那通常意味着MSVC是错误的。
Jonathan Wakely 2014年

1
注:一般在C ++,返回类型并没有参与到重载。
Matthieu M.

Answers:


51

标准规定:

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

§12.3[class.conv]

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

标准规定:

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

§13.3.3.1[over.match.funcs]

在这种情况下b,“隐含对象参数”为,类型为B2 &operator bool要求const B2 &,因此编译器将不得不b在调用中添加const operator bool。在其他所有条件都相同的情况下,这会operator int带来更好的匹配。

该标准指出,在以下情况下static_cast,a T(在这种情况下,C风格的强制转换正在执行)可以转换为类型(在这种情况下int):

该声明T t(e);格式正确,适用于某些发明的临时变量t

§5.2.9[expr.static.cast]

因此,int可以将转换为a bool,而将abool等效地转换为a bool

标准规定:

考虑的转换函数S及其基类。那些未隐藏在内部S且产生类型T 或可T通过标准转换序列转换为类型的类型的非显式转换函数是候选函数。

§13.3.1.5[over.match.conv]

因此,重载集由operator int和组成operator bool。在所有其他条件相同的情况下,这operator int是一个更好的匹配(因为您不必添加constness)。因此operator int应选择。

注意(一旦违反直觉),标准将不考虑返回类型(即这些运算符转换为的类型),一旦将它们添加到重载集(如上文所述)中,则提供以下项之一的参数的转换顺序它们优于另一个参数的转换顺序(由于恒定性,在这种情况下就是这种情况)。

标准规定:

给定这些定义,如果对于所有自变量i,ICSi(F1)的转换顺序都不比ICSi(F2)差,则将一个可行函数F1定义为比另一个可行函数F2更好的函数,然后

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

§13.3.3[最佳匹配]

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


2
您可以得到N3337,这是(我相信)是C ++ 11之后的第一稿,这里仅包含非常非常小的更改。至于查找标准的适当部分,通常只需判断PDF的各个部分,了解事物的位置(通过查找事物/引用标准),然后使用CTRL + F搜索相关的关键字。
罗伯特·艾伦·亨尼根·里希

1
@ikh在哪里可以找到当前的C或C ++标准文档?这是最好的问题,据我所知,它涵盖了可用于C和C ++的所有草案。
Shafik Yaghmour

2
这就是我所说的“仅在确定哪种转换顺序更好之后才应用”。但是,我认为这是答案的重要组成部分:在理论上,隐含对象参数的转换与“返回类型的转换”之间存在冲突,并且可以通过重载解析的不同步骤来明确解决。[over.match.best] /1.4明确指出了这些步骤的分离,以及为什么最终的标准转换顺序在这种情况下无关紧要。
dyp 2014年

2
我编辑了答案以纳入您的建议,详细说明了为什么在选择“最佳”答案时不考虑这些转换运算符的返回类型。
罗伯特·艾伦·亨尼根·里希

2
@RobertAllanHenniganLeahy既然是问题的重点,那么它不应该出现在答案中,而不仅仅是评论吗?
Barmar 2014年

9

转换函数operator int()由clang选择,operator bool() const因为b它不是const限定的,而bool的转换运算符是。

短的理由是,对于重载解析候选的功能(在适当位置隐式对象参数),在转换时bbool

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

第二个是更好的匹配,因为b它不是const限定的。

如果两个功能都具有相同的资格(const或不相同),operator bool则选择它,因为它可以直接转换。

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

如果我们同意布尔ostream的插入器(STD :: basic_ostream ::运算<<(布尔VAL),按照[ostream.inserters.arithmetic])被称为与值,从一个转换结果bbool我们可以挖成转换。

1.强制转换表达式

b转换为布尔

(bool)b

评估为

static_cast<bool>(b)

根据C ++ 11,5.4 / 4 [expr.cast],因为const_cast它不适用(此处未添加或删除const)。

如果对于本发明的变量t格式正确,则根据C ++ 11,5.2.9 / 4 [expr.static.cast]允许此静态转换bool t(b);。根据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 [...]假设“ cv1 T”是正在初始化的对象的类型,而“ cv S”是初始化表达式的类型,其中S为类类型,则按以下方式选择候选函数:考虑S的功能及其基类。那些未隐藏在S中并产生类型T或可以通过标准转换序列转换为类型T的类型的非显式转换函数是候选函数。

因此operator bool () const适用,因为它没有隐藏在其中B2并产生a bool

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

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

这意味着operator int ()同样适用。

2.3选择了哪种转换功能?

通过过载解析(C ++ 11,13.3.1.5/1 [over.match.conv])来选择适当的转换函数:

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

关于类成员函数的重载解析,有一个特殊的“怪癖”:“隐式对象参数”。

根据C ++ 11,13.3.1 [over.match.funcs]

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

对于非静态成员函数,此参数的类型-根据第4-节,为:

  • 不带ref限定符或带有&ref限定符声明的函数的“对cv X的左值引用”

  • 用&&​​ ref限定词声明的函数的“对cv X的右值引用”

其中X是该函数是成员的类,而cv是成员函数声明上的cv限定词。

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

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

解决过载的候选函数为:

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

显然,operator int ()如果B2由于operator bool ()要求进行资格转换而使用非常数类型的对象请求转换,则是更好的匹配。

如果两个转换函数共享相同的const限定符,那么这些函数的重载解析将不再有用。在这种情况下,转换(序列)排名就位。

3.operator bool ()两个转换函数共享相同的const资格时为什么选择?

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

用户定义的转换序列由初始标准转换序列,随后的用户定义转换和第二标准转换序列组成。

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

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

基于更好的转换序列和更好的转换之间的关系,定义隐式转换序列的部分排序。

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

第二标准转化箱子operator bool()boolbool在情况(标识转换),而所述第二标准的转换operator int ()intbool这是一个布尔值转换。

因此,operator bool ()如果两个转换函数共享相同的const限定,则使用的转换顺序会更好。


在大多数情况下,很直观地知道您对结果进行的操作不会影响其计算方式。a/b无论代码是float f = a/b;还是,我们的计算方式都是相同的int f = a/b;。但是在这种情况下,可能会有些令人惊讶。
大卫·史瓦兹

1

C ++ bool类型具有两个值-真和假,对应的值分别为1和0。如果在B2类中添加显式调用基类(B)的bool运算符的bool运算符,则可以避免固有的混淆,然后输出为假。这是我修改过的程序。然后,运算符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类本身中明确拥有布尔运算符,可以解决此问题。


8
这是编写程序的一个不错的解决方案,但是没有回答为什么这种奇怪的事情会详细发生。
ikh 2014年

4
true and false with corresponding values 1 and 0这太简单了。true转换为整数1,但这并不意味着它“具有”值1。确实,true可能在42下面。
Lightness Races in Orbit

在B2中具有显式的布尔运算符可避免在B2中调用运算符int或运算符bool造成混淆和编译器依赖性。
Debasish Jana博士2014年

没有编译器依赖性。标准要求0转换为false,1转换为true。
小狗

1
@LightnessRacesinOrbit更确切地说,也许是:abool从不具有0或1的值;唯一可以取的值是falseand true(如果将bool转换为整数类型,则将转换为0和1 )。
James Kanze 2014年

0

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

我的贡献是,“ cast操作”的编译方式与“重载操作”类似,我建议为每个操作制作一个具有唯一标识符的函数,然后再用所需的运算符或强制转换将其替换。

#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;
}

只是我的2美分。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.