构造虚拟基类时的编译器行为

Posted

技术标签:

【中文标题】构造虚拟基类时的编译器行为【英文标题】:Compiler behaviour when constructing virtual base class 【发布时间】:2016-11-18 09:35:43 【问题描述】:

考虑这段代码:

#include <iostream>

class A 
  public:
    A(int s)  std::cout << "A(" << s << ")\n"; 
;

class B1 : virtual public A 
  public:
    B1(int s1, int s2)
        : As1  std::cout << "B1(" << s1 << "," << s2 << ")\n"; 
;

class B2 : virtual public A 
  public:
    B2(int s1, int s2)
        : As1  std::cout << "B2(" << s1 << "," << s2 << ")\n"; 
;

class C1 : public B1, public B2 
  public:
    C1() : B11,2, B23,4, A5 
;

class C2 : public B1, public B2 
  public:
    C2() : B1(1,2), B2(3,4), A5 
;

int main()

    std::cout << "Create c1:\n";
    C1 c1;

    std::cout << "\n";
    std::cout << "Create c2:\n";
    C2 c2;


A 类是 B1 和 B2 的虚拟基类。 C1 和 C2 类是相同的,只是 C1 使用 ... 而 C2 使用 (...) 来构造 B1 和 B2。

由于这里使用了虚继承,所以类 A 应该作为 C1 或 C2 构造的一部分来构造。

如果我使用 Microsoft VS2015 编译此代码,它会在运行时产生以下输出:

Create c1:
A(5)
B1(1,2)
B2(3,4)

Create c2:
A(5)
B1(1,2)
B2(3,4)

这正是我所期望的。

但是如果我用 GCC (6.1.0) 编译它,它会产生这个输出:

Create c1:
A(5)
A(1)
B1(1,2)
A(3)
B2(3,4)

Create c2:
A(5)
B1(1,2)
B2(3,4)

这里,A的构造函数在构造c1的时候被调用了3次,而在构造c2的时候只调用了一次。

这是 GCC 中的错误还是我误解了什么?

【问题讨论】:

您可以考虑添加常规的 C++ 标签,以获得更广泛的受众。 好的,@Jonas,我用 c++ 替换了 c++11 标签。 (我的标签不能超过 5 个。) 你也试过 Clang 吗?我 99% 确定这是一个错误。 这是个bug,好像在trunk中修复了。 老实说,如果我得到一个接受的答案只是为了将您的代码粘贴到 Wandbox 中,我会感到难过 :-)。基本上,你已经完成了所有的工作,并且做得很好。以下答案之一至少添加了更多信息。 【参考方案1】:

回答我自己的问题:

显然,GCC 在这种情况下确实存在错误。使用 GCC 版本 7.0.0 编译代码会产生正确的输出行为:

Create c1:
A(5)
B1(1,2)
B2(3,4)

Create c2:
A(5)
B1(1,2)
B2(3,4)

【讨论】:

【参考方案2】:

不太清楚为什么会这样,但一个类不应该是多态的(至少有一个虚函数)才能正确地从它虚拟继承吗?!

【讨论】:

用 A 多态尝试过,用 gcc 5.4.0,同样的错误。 有趣的是,gcc 的 sizeof(c1) 和 sizeof(c2) 是相同的,如果在 A 中声明了一个方法,则可以从 c1 或 c2 毫无歧义地调用它。 虚拟基类通常是多态的,作为抽象接口类。但是作为基类的属性是派生类内部的属性;类型只是派生类中的虚拟或非虚拟基类。【参考方案3】:

这个错误要严重得多:

    #include <iostream>

    class A 
       public:
          A(int s)  std::cout << "A(" << s << ")\n"; ms = s; ;
          int ms;
          virtual void dummy() std::cout << "Aaaaaa!" << std::endl;;
          virtual ~A() std::cout << "~A(" << ms << ")\n";;
    ;

    class B1 : virtual public A 
        public:
           B1(int s1, int s2)
              : As1  std::cout << "B1(" << s1 << "," << s2 << ")\n"; ;
    ;

    class B2 : virtual public A 
        public:
           B2(int s1, int s2)
              : As1  std::cout << "B2(" << s1 << "," << s2 << ")\n"; ;
    ;

       class C1 : public B1, public B2 
           public:
              C1() : B11,2, B23,4, A5  ;
       ;

       class C2 : public B1, public B2 
           public:
              C2() : B1(1,2), B2(3,4), A5 ;
       ;

      int main()
      
         
             std::cout << "Create c1:\n";
             C1 c1;
             std::cout << "Calling A's dummy on c1: " << std::endl;
             c1.dummy();
             std::cout << "Size of c1: " << sizeof(c1) << std::endl;
         

         
             std::cout << "\n";
             std::cout << "Create c2:\n";
             C2 c2;
             std::cout << "Calling A's dummy on c2: " << std::endl;
             c2.dummy();
             std::cout << "Size of c2: " <<sizeof(c2) << std::endl;
         

     

在 GCC 5.4.0 上编译 运行此代码会产生以下输出:

创建 c1: A(5) A(1) B1(1,2) A(3) B2(3,4) 在 c1 上调用 A 的假人: 啊啊啊! c1 的大小:32 ~A(3)
创建 c2: A(5) B1(1,2) B2(3,4) 在 c2 上调用 A 的假人: 啊啊啊! c2 的大小:32 ~A(5)

【讨论】:

您在这里发现了什么新的错误?除了我在原帖中提到的错误,据我所知,您的代码完全符合预期。 bug 是一样的,你是对的。只是想指向 A 的析构函数调用。

以上是关于构造虚拟基类时的编译器行为的主要内容,如果未能解决你的问题,请参考以下文章

Java面试

使用实体基类时的 EF CTP5 映射问题

测试扩展类时的 ES6 基类(超级方法)的玩笑模拟方法

继承基类时如何避免错误“找不到类型'MyType'的构造函数”

c++ 接口必须遵守五法则吗?

有其他虚拟基类时如何避免对基类函数的模糊调用