是否可以使用虚拟继承来防止意外创建钻石?

时间:2009-09-09 22:05:38

标签: c++ inheritance virtual dynamic-cast diamond-problem

这是一些真实代码的简化,当我没有意识到其他人已经实现了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;
}

编辑:以免您必须运行此代码...

  • 如果按原样运行,则会输出“fail”(不合适,难以调试)。
  • 如果删除标有“oops”的行,则会打印“Foo”(所需行为)。
  • 如果您离开“oops”并使两个继承成为虚拟,它将无法编译(但至少您知道要修复的内容)。
  • 如果删除“oops”并将其设为虚拟,则会编译并打印“Foo”(所需行为)。

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


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

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

但是如果我从Base派生时使用了虚拟继承,那么代码也可以正常工作(没有我的oops),我会在编译时被警告过钻石而不是在运行时追踪错误时间。

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

3 个答案:

答案 0 :(得分:2)

不是个好主意。

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

哦,除非事情发生了变化,否则在没有基类帮助的情况下,你不能将虚拟基类转发给任何派生类。

答案 1 :(得分:2)

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

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

问题是运行时的动态强制转换有一个指向Derived对象的指针。但是从这里到Base是一个暧昧的操作,因此失败了。没有编译器错误,因为dynamic_cast是运行时操作。 (您可以尝试从任何内容转换为任何内容,结果在失败时为NULL(如果使用引用,则抛出)。)

两个选项:

  • 如果你投射引用,你可以让dynamic_cast抛出异常。
  • 或者您可以使用在编译时检查的强制转换:static_cast&lt;&gt;

用这个查看:

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.

答案 2 :(得分:1)

你应该首先考虑的是inheritance is not for code reuse,所以当从两个基础继承共同的祖先和双方实施的方法时,请认为它是两次。

如果您认为您确实想要继承两个基础,那么您将需要使用虚拟继承而不是复制祖先。这在实施exception hierarchies时很常见。请注意,虚拟基础由最派生类型的构造函数直接初始化,需要关注它。