C++ 强制转换运算符重载和多态性

Posted

技术标签:

【中文标题】C++ 强制转换运算符重载和多态性【英文标题】:C++ cast operator overloading and polymorhism 【发布时间】:2012-12-27 05:57:34 【问题描述】:

我对 C++ 的这种行为感到困惑:

struct A 
   virtual void print() const  printf("a\n"); 
;

struct B : public A 
   virtual void print() const  printf("b\n"); 
;

struct C 
   operator B()  return B(); 
;

void print(const A& a) 
   a.print();


int main() 
   C c;
   print(c);

那么,测验是,程序的输出是什么 - a 还是 b?嗯,答案是一个。但为什么呢?

【问题讨论】:

它在我的机器上打印b。另外,void main()?酒吧。 main 返回int,伙计 @Carl:什么编译器?参见例如ideone.com/4W7qIa(即 GCC 4.3.4)。 GCC 4.5.1 也像 VS2010 一样打印“a”。 @Code-Guru:那是不是operator(),而是到B的转换运算符。注意区别:B operator();operator B() 通过 copy-initialization 进行切片,生成 "a",如果省略则生成 "b"。这被 [5.8.3]/5 所涵盖,但我无法弄清楚... 【参考方案1】:

这里的问题是 C++03 标准中的一个错误/错误功能/漏洞,不同的编译器试图以不同的方式修补该问题。 (这个问题在 C++11 标准中已经不存在了。)

这两个标准的第 8.5.3/5 节指定了如何初始化引用。这是 C++03 版本(列表编号是我的):

cv1 T1 类型的引用由cv2 T2 类型的表达式初始化,如下所示:

    如果初始化表达式

      是一个左值(但不是位域),并且“cv1 T1”与“cv2 T2”是引用兼容的,或者 具有类类型(即T2是类类型)并且可以隐式转换为cv3 T3类型的左值,其中cv1 T1cv3 T3是引用兼容的

    那么第一种情况下引用直接绑定到初始化表达式左值,第二种情况下引用绑定到转换的左值结果。

    否则,引用应为非易失 const 类型(即,cv1 应为 const)。

    如果初始化表达式是右值,T2 是类类型,并且cv1 T1cv2 T2 引用兼容,则引用以下列方式之一绑定(选择由实现定义) :

      引用绑定到右值表示的对象(参见 3.10)或该对象中的子对象。 创建了cv1 T2 [sic] 类型的临时对象,并调用构造函数将整个右值对象复制到临时对象中。引用绑定到临时对象或临时对象中的子对象。

    无论复制是否实际完成,用于制作复制的构造函数都应该是可调用的。

    否则,将使用非引用副本初始化 (8.5) 的规则从初始化表达式创建并初始化 cv1 T1 类型的临时变量。然后将引用绑定到临时对象。

手头的问题涉及三种类型:

要创建的引用的类型。标准(两个版本)将此类型表示为T1。在这种情况下,它是struct A。 初始化表达式的类型。标准将此类型表示为T2。在这种情况下,初始化表达式是变量c,所以T2struct C。请注意,由于struct Astruct C引用兼容,因此无法直接将引用绑定到c。需要一个中间人。 中间体的类型。标准将此类型表示为T3。在这种情况下,这是struct B。请注意,将转换运算符 C::operator B() 应用于 c 会将左值 c 转换为右值。

我标记为 1.1 和 3 的初始化已失效,因为 struct Astruct C 的引用不兼容。需要使用转换运算符C::operator B()。 1.2 out 因为这个转换运算符返回一个右值,所以这个规则 1.2 out。剩下的就是选项 4,创建一个 cv1 T1 类型的临时变量。严格遵守 2003 版标准会强制为这个问题创建两个临时文件,即使只有一个就足够了。

2011 版标准通过将选项 3 替换为

解决了该问题

如果初始化表达式

是一个 xvalue、类纯右值、数组纯右值或函数左值,cv1 T1 是引用- 与cv2 T2 兼容,或 具有类类型(即T2 是类类型),其中T1T2 没有引用相关,并且可以隐式转换为类型的xvalue、类纯右值或函数左值cv3 T3,其中cv1 T1cv3 T3 引用兼容,

那么引用在第一种情况下绑定到初始化表达式的值,在第二种情况下绑定到转换的结果(或者,在任何一种情况下,都绑定到适当的基类子对象)。在第二种情况下,如果引用是右值引用,并且用户定义的转换序列的第二个标准转换序列包括左值到右值的转换,则程序是非良构的。

似乎 gcc 系列编译器选择了严格遵从而不是意图(避免创建不必要的临时变量),而其他打印“b”的编译器选择了对标准的意图/更正。选择严格合规不一定值得称道。在 2003 版标准(例如,std::set)中还有其他错误/错误功能,其中 gcc 家族选择理智而不是严格遵守。

【讨论】:

+1:但是,我无法理解您所说的(avoid creating unnecessary temporaries) 是什么意思。在我看来,or, in either case, to an appropriate base class subobject 部分允许 gcc 打印“a”而不是“b”。 @JesseGood - 程序的 gcc 编译版本创建两个临时对象,一个是 B 类型,调用 C::operator B(),另一个是 A 类型,通过复制构造。第二个暂时是完全没有必要的。因为创建不必要的临时对象会影响应用程序的性能,所以避免创建它们很重要。例如,Eigen 使用表达式模板进行惰性求值以避免不必要的临时性。一段时间以来,标准委员会一直在寻找标准不必要地要求不必要的临时性的地方。 我现在明白了。 gcc 仍然遵循 C++03 规则。我找到了一个相关的defect report,如果提议的决议通过,那么 gcc 将再次正确(尽管目前正在审查中,似乎并不是每个人都同意该怎么做)。

以上是关于C++ 强制转换运算符重载和多态性的主要内容,如果未能解决你的问题,请参考以下文章

Part8 多态性 8.1运算符重载

如何在 C++ 中重载多态 == 和 != 运算符

在C++中,啥是运算符重载?啥是虚函数?

C++基础——C++面向对象之重载与多态基础总结(函数重载运算符重载多态的使用)

多态性——运算符重载

重载()运算符和重载强制类型转换