【问题标题】:Different cast operator called by different compilers不同编译器调用的不同类型转换运算符
【发布时间】:2014-10-04 04:15:39
【问题描述】:

考虑以下简短的 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-conversion 更“昂贵”?

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

【问题讨论】:

  • 据我了解,B2 有 2 个运算符:B::operator bool() 和 B2::operator int()。两个运算符都不是 const 运算符,并且 const 和非 const 版本之间存在差异。您可以将 bool 的 const 版本添加到 B 并将 int 的 const 版本添加到 B2 并查看更改。
  • const 是这里的关键。两个运算符是否相同,它们的行为符合预期,bool 版本“获胜”。使用不同的 constness,非 const 版本总是在每个平台上“获胜”。由于派生类和运算符的不同常量,VS 似乎有一个错误,因为它的行为与其他两个编译器不同。
  • 不同的默认构造函数会负责不同的行为吗?
  • EDG 也打印true。如果 GCC、Clang 和 EDG 都同意而 MSVC 不同意,这通常意味着 MSVC 是错误的。
  • 注意:通常在 C++ 中,返回类型参与重载决议。

标签: c++ language-lawyer


【解决方案1】:

标准规定:

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

§12.3 [class.conv]

这意味着operator bool 没有被operator int 隐藏。

标准规定:

在重载决议期间,隐含的对象参数与​​其他参数无法区分。

§13.3.3.1 [over.match.funcs]

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

标准规定 static_cast(在本例中执行 C 风格转换)可以转换为类型 T(在本例中为 int),如果:

声明T t(e); 是格式良好的,对于一些发明的临时变量t

§5.2.9 [expr.static.cast]

因此int 可以转换为boolbool 也可以同样转换为bool

标准规定:

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

§13.3.1.5 [over.match.conv]

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

请注意(可能违反直觉)标准不考虑返回类型(即这些运算符转换为的类型)一旦它们被添加到重载集(如上所述),提供参数的转换顺序其中一个的参数优于另一个参数的转换顺序(由于 constness,在本例中就是这种情况)。

标准规定:

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

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

§13.3.3 [over.match.best]

在这种情况下,只有一个参数(隐式this 参数)。 B2 &amp; => B2 &amp;(调用operator int)的转换顺序优于B2 &amp; => const B2 &amp;(调用operator bool),因此operator int是从没有关于它实际上并没有直接转换为bool这一事实。

【讨论】:

  • 您可以免费获得 N3337,它(我相信)是 C++11 之后的第一个草案,并且只包含非常非常小的更改 here。至于找到标准的适当部分,通常是判断 PDF 的部分,了解事物的位置(通过查找/引用标准),并使用 CTRL+F 搜索相关关键字。
  • @ikh Where do I find the current C or C++ standard documents? 是最好的问题,据我所知,涵盖了所有可用于 C 和 C++ 的草案。
  • 这就是我所说的“仅在确定哪个转换顺序更好之后应用”。然而,我认为这是答案的一个重要部分:隐含对象参数的转换和“返回类型的转换”之间存在(理论上)冲突,并且通过重载解析的不同步骤明确解决。 [over.match.best]/1.4 清楚地指出了这些步骤的分离,以及为什么在这种情况下最终的标准转换序列并不重要。
  • 我编辑了答案以纳入您的建议,详细说明了为什么在选择“最佳”时不考虑这些转换运算符的返回类型。
  • @RobertAllanHenniganLeahy 既然这是问题的重点,它不应该出现在答案的某处,而不仅仅是评论吗?
【解决方案2】:

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

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

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

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

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

通过cast-notation转换,逐步分析

如果我们同意使用从b 转换为bool 我们可以深入研究这种转换。

1。演员表

b 到 bool 的转换

(bool)b

计算为

static_cast<bool>(b)

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

如果发明变量 t 的 bool t(b); 格式正确,则根据 C++11, 5.2.9/4 [expr.static.cast] 允许这种静态转换。 根据 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 中并产生bool

最后一个标准引号中强调的部分与使用operator int () 的转换相关,因为int 是一种可以通过标准转换序列转换为bool 的类型。 从intbool 的转换甚至不是一个序列,而是一个简单的直接转换,这在 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.1 [over.match.funcs],

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

根据第 4 条,非静态成员函数的此参数的类型是:

  • “对 cv X 的左值引用”适用于未使用 ref-qualifier 或 & ref-qualifier 声明的函数

  • 使用 && ref-qualifier 声明的函数的“对 cv X 的右值引用”

其中 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 () 需要限定转换。

如果两个转换函数共享相同的 const 限定条件,则这些函数的重载决议将不再有效。 在这种情况下,转换(序列)排名就到位了。

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

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

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

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

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

[...] 根据更好的转换序列和更好的转换之间的关系,定义了隐式转换序列的偏序。

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

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

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

【讨论】:

  • 在大多数情况下,您对结果所做的操作并不影响其计算方式,这一点非常直观。无论代码是float f = a/b; 还是int f = a/b;,我们计算a/b 的方式都是一样的。但这种情况可能有点令人惊讶。
【解决方案3】:

C++ bool 类型有两个值 - true 和 false,对应的值为 1 和 0。如果在 B2 类中添加一个显式调用基类 (B) 的 bool 运算符的 bool 运算符,则可以避免固有的混淆,然后输出为假。 这是我修改后的程序。那么 operator bool 意味着 operator bool 而不是 operator 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 运算符,问题就得到了解决。

【讨论】:

  • 写程序是个不错的解决方案,但是没有详细回答为什么会出现这种奇怪的事情。
  • true and false with corresponding values 1 and 0 过于简单化了。 true 转换为整数 1,但这并不意味着它“具有”值 1。事实上,true 可能是42 下面。
  • 在 B2 中使用显式 bool 运算符可避免 B2 中调用的操作符 int 或 operator bool 的混淆和编译器依赖性。
  • 没有编译器依赖。标准要求 0 转换为 false,1 转换为 true。
  • @LightnessRacesinOrbit 更重要的是,也许:bool 永远不会有值 0 或 1;它可以采用的唯一值是falsetrue(如果bool 转换为整数类型,则转换为0 和1)。
【解决方案4】:

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

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

#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 美分。

【讨论】:

    猜你喜欢
    • 2013-05-14
    • 1970-01-01
    • 2023-03-28
    • 1970-01-01
    • 2014-07-28
    • 2020-11-09
    • 2018-05-22
    • 1970-01-01
    • 2012-01-09
    相关资源
    最近更新 更多