可以接受使用虚拟继承来防止意外创建钻石吗?

Posted

技术标签:

【中文标题】可以接受使用虚拟继承来防止意外创建钻石吗?【英文标题】:Acceptable to use virtual inheritance to prevent accidentally creating a diamond? 【发布时间】:2009-09-09 22:05:38 【问题描述】:

这是对一些真实代码的简化,当我没有意识到其他人已经实现了 Foo 并从它派生时,我犯了一个真正的错误。

#include <iostream>

struct Base 
   virtual ~Base()  
   virtual void print() = 0;
;

struct OtherBase 
   virtual ~OtherBase()  
;

struct Foo : public Base  // better to use virtual inheritance?
   virtual void print()  std::cout << "Foo" << std::endl; ;
;

struct Bar : public Base  // better to use virtual inheritance?
   virtual void print()  std::cout << "Bar" << std::endl; ;
;

// The design is only supposed to implement Base once, but I
// accidentally created a diamond when I inherited from Bar also.
class Derived
   : public OtherBase
   , public Foo
   , public Bar // oops.

;

int main() 
   Derived d;
   OtherBase *pO = &d;

   // cross-casting
   if (Base *pBase = dynamic_cast<Base *>(pO))
      pBase->print();
   else
      std::cout << "fail" << std::endl;

编辑:让您不必运行此代码...

如果按原样运行,则会显示“失败”(不受欢迎,难以调试)。 如果您删除标记为“oops”的行,它会打印“Foo”(期望的行为)。 如果您离开“oops”并将两个继承设置为虚拟,它将无法编译(但至少您知道要修复什么)。 如果您删除“oops”并将它们设为虚拟,它将编译并打印“Foo”(期望的行为)。

使用虚拟继承,结果要么是好的,要么是编译器错误。如果没有虚拟继承,结果要么是好的,要么是无法解释的、难以调试的运行时故障。


当我实现 Bar 时,它基本上复制了 Foo 已经在做的事情,它导致动态转换失败,这意味着真实代码中的坏事。

起初我很惊讶没有编译器错误。然后我意识到没有虚拟继承,这会触发 GCC 中的“没有唯一的最终覆盖”错误。我故意选择不使用虚拟继承,因为这个设计中不应该有任何菱形。

但是,如果我在从 Base 派生时使用虚拟继承,代码也可以正常工作(没有我的 oops),并且我会在编译时收到有关菱形的警告,而不必在运行时跟踪错误时间。

所以问题是——你认为使用虚拟继承来防止将来犯类似的错误是可以接受的吗?在这里使用虚拟继承没有很好的技术理由(我可以看到),因为设计中不应该有钻石。它只会在那里强制执行该设计约束。

【问题讨论】:

【参考方案1】:

不是个好主意。

只有在事先计划好的情况下才能使用虚拟继承。正如您刚刚发现的那样,在许多情况下,所有后代类都必须知道它。如果基类有一个非默认构造函数,你必须担心它总是由叶类构造。

哦,除非我上次查看后情况发生了变化,否则在没有基类帮助的情况下,您不能将虚拟基类向下转换为任何派生类。

【讨论】:

【参考方案2】:

这里没有钻石! 您创建的是多重继承。每个 Base 类都有自己的 Base 副本。

pO 的类型为 OtherBase*。 无法将 OtherBase* 的对象转换为 Base* 类型。 因此动态转换将返回一个 NULL 指针。

问题在于运行时的动态转换有一个指向 Derived 对象的指针。但是从这里到 Base 是一个模棱两可的操作,因此失败并返回 NULL。没有编译器错误,因为 dynamic_cast 是运行时操作。 (您可以尝试将任何内容转换为任何结果,失败时为 NULL(或使用引用时抛出)。

两种选择:

如果你强制转换引用,你可以让 dynamic_cast 抛出异常。 或者您可以使用在编译时检查的强制转换 static_cast

用这个检查一下:

struct Base

    Base(int x): val(x) 
    int val;
...

struct Foo : public Base

    Foo(): Base(1)  
.... 

struct Bar : public Base

    Bar(): Base(2)  
....


// In main:
    std::cout << "Foo" << dynamic_cast<Foo&>(d).val << "\n"
              << "Bar" << dynamic_cast<Bar&>(d).val << "\n";


> ./a.exe  
fail
Foo1
Bar2

编译时间检查:

std::cout << static_cast<Base*>(pO) << "\n"; // Should fail to compile.
std::cout << static_cast<Base*>(&d) << "\n"; // Will only fail if ambigious.
                                             // So Fails if Foo and Bar derived from Base
                                             // But works if only one is derived.

【讨论】:

"这里没有钻石!" -- 好的,所以如果我将遗产虚拟化,这在技术上只是一颗钻石。好点子。问题仍然存在:将继承设为虚拟以防止我犯的那种错误是否可以接受? “没有办法将 OtherBase* 的对象转换为 Base* 类型”——你错了,试试看。可以使用 dynamic_cast 跨层次结构进行转换。 @丹。当只有一个 Foo/Bar 继承自 Base 时,它​​可以工作。这是因为 dynamic_cast 是运行时强制转换(没有任何编译时信息可用(只有运行时信息))。因此,动态案例是将 Derived 类型的对象转换为 Base 类型。这显然是允许的。 PS 术语“跨层级转换”具有误导性,您在标准中的任何地方都找不到该术语。您只是在使用运行时信息向下(或向上)层次结构。 如果必须只为 Derived 定义一次 Base。然后你应该看看 Boosts 静态断言。我相信你可以找到一种方法来添加编译时间检查。 谢谢 Martin,我会看看 Boost 是否可行。回复:“交叉铸造”,我不知道还能叫什么——这里使用的类似术语:objectmentor.com/resources/articles/crosscst.pdf(见第 4 和 10 页)。【参考方案3】:

您首先应该考虑的是inheritance is not for code reuse,因此在从具有共同祖先和双方都实现的方法的两个基类继承时要三思。

如果你认为你真的想继承两个基类,你会想要使用虚拟继承而不是复制祖先。这在实现exception hierarchies 时很常见。请注意,虚拟基由最衍生类型的构造函数直接初始化,需要注意这一点。

【讨论】:

以上是关于可以接受使用虚拟继承来防止意外创建钻石吗?的主要内容,如果未能解决你的问题,请参考以下文章

我的继承和有问题的“钻石继承”一样吗

虚拟继承如何解决“钻石”(多重继承)的歧义?

C++菱形继承问题与虚拟继承原理

C++菱形继承问题与虚拟继承原理

虚拟多重继承和强制转换

当涉及部分虚拟继承时,这是计算创建的基类对象数量的正确方法吗?