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

发布于 2024-08-29 02:42:57 字数 603 浏览 6 评论 0原文

class A                     { public: void eat(){ cout<<"A";} }; 
class B: virtual public A   { public: void eat(){ cout<<"B";} }; 
class C: virtual public A   { public: void eat(){ cout<<"C";} }; 
class D: public         B,C { public: void eat(){ cout<<"D";} }; 

int main(){ 
    A *a = new D(); 
    a->eat(); 
} 

我理解钻石问题,上面的代码没有这个问题。

虚拟继承到底是如何解决问题的呢?

我的理解: 当我说 A *a = new D(); 时,编译器想知道 D 类型的对象是否可以分配给 A 类型的指针,但它有两条可以遵循的路径,但不能自行决定。

那么,虚拟继承如何解决这个问题(帮助编译器做出决定)?

class A                     { public: void eat(){ cout<<"A";} }; 
class B: virtual public A   { public: void eat(){ cout<<"B";} }; 
class C: virtual public A   { public: void eat(){ cout<<"C";} }; 
class D: public         B,C { public: void eat(){ cout<<"D";} }; 

int main(){ 
    A *a = new D(); 
    a->eat(); 
} 

I understand the diamond problem, and above piece of code does not have that problem.

How exactly does virtual inheritance solve the problem?

What I understand:
When I say A *a = new D();, the compiler wants to know if an object of type D can be assigned to a pointer of type A, but it has two paths that it can follow, but cannot decide by itself.

So, how does virtual inheritance resolve the issue (help compiler take the decision)?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(5

佼人 2024-09-05 02:42:57

你想要:(可以通过虚拟继承实现)

  A  
 / \  
B   C  
 \ /  
  D 

而不是:(没有虚拟继承会发生什么)

A   A  
|   |
B   C  
 \ /  
  D 

虚拟继承意味着只有 1 个基 A 实例 类不是 2。

您的类型 D 将有 2 个 vtable 指针(您可以在第一个图中看到它们),一个用于 B,一个用于 C 实际上继承了 AD 的对象大小增加了,因为它现在存储了 2 个指针;然而现在只有一个A

因此,B::AC::A 是相同的,因此来自 D 的调用不会有歧义。如果您不使用虚拟继承,您将看到上面的第二张图。然后,对 A 成员的任何调用都会变得不明确,您需要指定要采用的路径。

维基百科在这里还有另一个很好的概述和示例

You want: (Achievable with virtual inheritance)

  A  
 / \  
B   C  
 \ /  
  D 

And not: (What happens without virtual inheritance)

A   A  
|   |
B   C  
 \ /  
  D 

Virtual inheritance means that there will be only 1 instance of the base A class not 2.

Your type D would have 2 vtable pointers (you can see them in the first diagram), one for B and one for C who virtually inherit A. D's object size is increased because it stores 2 pointers now; however there is only one A now.

So B::A and C::A are the same and so there can be no ambiguous calls from D. If you don't use virtual inheritance you have the second diagram above. And any call to a member of A then becomes ambiguous and you need to specify which path you want to take.

Wikipedia has another good rundown and example here

榆西 2024-09-05 02:42:57

为什么还有另一个答案?

好吧,SO 上的许多帖子和外部文章都说,钻石问题是通过创建 A 的单个实例而不是两个(D 的每个父级一个)来解决的,从而解决了歧义。然而,这并没有让我对流程有全面的了解,我最终遇到了更多问题,比如

  1. 如果 BC 尝试创建 A 的不同实例会怎样? 例如,使用不同的参数调用参数化构造函数(D::D(int x, int y): C(x), B(y) {})? A 的哪个实例将被选择成为 D 的一部分?
  2. 如果我对 B 使用非虚拟继承,但对 C 使用虚拟继承,会怎么样?在D中创建A的单个实例是否足够?
  3. 从现在开始,我是否应该始终默认使用虚拟继承作为预防措施,因为它以较小的性能成本解决了可能的钻石问题,并且没有其他缺点?

如果不尝试代码示例就无法预测行为意味着不理解这个概念。下面是帮助我了解虚拟继承的内容。

Double A

首先,让我们从这段没有虚拟继承的代码开始:

#include<iostream>
using namespace std;
class A {
public:
    A()                { cout << "A::A() "; }
    A(int x) : m_x(x)  { cout << "A::A(" << x << ") "; }
    int getX() const   { return m_x; }
private:
    int m_x = 42;
};

class B : public A {
public:
    B(int x):A(x)   { cout << "B::B(" << x << ") "; }
};

class C : public A {
public:
    C(int x):A(x) { cout << "C::C(" << x << ") "; }
};

class D : public C, public B  {
public:
    D(int x, int y): C(x), B(y)   {
        cout << "D::D(" << x << ", " << y << ") "; }
};

int main()  {
    cout << "Create b(2): " << endl;
    B b(2); cout << endl << endl;

    cout << "Create c(3): " << endl;
    C c(3); cout << endl << endl;

    cout << "Create d(2,3): " << endl;
    D d(2, 3); cout << endl << endl;

    // error: request for member 'getX' is ambiguous
    //cout << "d.getX() = " << d.getX() << endl;

    // error: 'A' is an ambiguous base of 'D'
    //cout << "d.A::getX() = " << d.A::getX() << endl;

    cout << "d.B::getX() = " << d.B::getX() << endl;
    cout << "d.C::getX() = " << d.C::getX() << endl;
}

让我们看一下输出。执行 B b(2); 按预期创建 A(2),与 C c(3); 相同:

Create b(2): 
A::A(2) B::B(2) 

Create c(3): 
A::A(3) C::C(3) 

D d (2, 3); 需要 BC,它们各自创建自己的 A,所以我们有两个 < d中的code>A:

Create d(2,3): 
A::A(2) C::C(2) A::A(3) B::B(3) D::D(2, 3) 

这就是d.getX()导致编译错误的原因,因为编译器无法选择哪个A 它应该调用方法的实例。仍然可以直接调用所选父类的方法:

d.B::getX() = 3
d.C::getX() = 2

虚拟性

现在让我们添加虚拟继承。使用相同的代码示例并进行以下更改:

class B : virtual public A
...
class C : virtual public A
...
cout << "d.getX() = " << d.getX() << endl; //uncommented
cout << "d.A::getX() = " << d.A::getX() << endl; //uncommented
...

让我们跳转到 d 的创建:

Create d(2,3): 
A::A() C::C(2) B::B(3) D::D(2, 3) 

您可以看到,A 是使用默认构造函数创建的,忽略从 B 构造函数传递的参数C。由于歧义性消失,对 getX() 的所有调用都会返回相同的值:

d.getX() = 42
d.A::getX() = 42
d.B::getX() = 42
d.C::getX() = 42

但是如果我们想为 A 调用参数化构造函数该怎么办?可以通过从 D 的构造函数显式调用它来完成:

D(int x, int y, int z): A(x), C(y), B(z)

通常,类只能显式使用直接父级的构造函数,但虚拟继承情况除外。发现这条规则对我来说“很有意义”,并且对理解虚拟接口有很大帮助:

代码 class B: virtual A 意味着,任何从 B 继承的类现在都负责用于自行创建 A,因为 B 不会自动执行此操作。

记住此声明后,很容易回答我遇到的所有问题

  1. D 的创建 BC 都不负责 A 的参数,完全取决于 D<仅/代码>。
  2. C 会将 A 的创建委托给 D,但 B 将创建自己的 A< 实例/code> 从而使钻石问题重新出现
  3. 在孙子类而不是直接子类中定义基类参数不是一个好的做法,因此当钻石问题存在且此措施不可避免时应该容忍。

Why another answer?

Well, many posts on SO and articles outside say, that diamond problem is solved by creating single instance of A instead of two (one for each parent of D), thus resolving ambiguity. However, this didn't give me comprehensive understanding of process, I ended up with even more questions like

  1. what if B and C tries to create different instances of A e.g. calling parametrized constructor with different parameters (D::D(int x, int y): C(x), B(y) {})? Which instance of A will be chosen to become part of D?
  2. what if I use non-virtual inheritance for B, but virtual one for C? Is it enough for creating single instance of A in D?
  3. should I always use virtual inheritance by default from now on as preventive measure since it solves possible diamond problem with minor performance cost and no other drawbacks?

Not being able to predict behavior without trying code samples means not understanding the concept. Below is what helped me to wrap head around virtual inheritance.

Double A

First, lets start with this code without virtual inheritance:

#include<iostream>
using namespace std;
class A {
public:
    A()                { cout << "A::A() "; }
    A(int x) : m_x(x)  { cout << "A::A(" << x << ") "; }
    int getX() const   { return m_x; }
private:
    int m_x = 42;
};

class B : public A {
public:
    B(int x):A(x)   { cout << "B::B(" << x << ") "; }
};

class C : public A {
public:
    C(int x):A(x) { cout << "C::C(" << x << ") "; }
};

class D : public C, public B  {
public:
    D(int x, int y): C(x), B(y)   {
        cout << "D::D(" << x << ", " << y << ") "; }
};

int main()  {
    cout << "Create b(2): " << endl;
    B b(2); cout << endl << endl;

    cout << "Create c(3): " << endl;
    C c(3); cout << endl << endl;

    cout << "Create d(2,3): " << endl;
    D d(2, 3); cout << endl << endl;

    // error: request for member 'getX' is ambiguous
    //cout << "d.getX() = " << d.getX() << endl;

    // error: 'A' is an ambiguous base of 'D'
    //cout << "d.A::getX() = " << d.A::getX() << endl;

    cout << "d.B::getX() = " << d.B::getX() << endl;
    cout << "d.C::getX() = " << d.C::getX() << endl;
}

Lets go through output. Executing B b(2); creates A(2) as expected, same for C c(3);:

Create b(2): 
A::A(2) B::B(2) 

Create c(3): 
A::A(3) C::C(3) 

D d(2, 3); needs both B and C, each of them creating its own A, so we have double A in d:

Create d(2,3): 
A::A(2) C::C(2) A::A(3) B::B(3) D::D(2, 3) 

That's the reason for d.getX() to cause compilation error as compiler can't choose which A instance it should call method for. Still it's possible to call methods directly for chosen parent class:

d.B::getX() = 3
d.C::getX() = 2

Virtuality

Now lets add virtual inheritance. Using same code sample with the following changes:

class B : virtual public A
...
class C : virtual public A
...
cout << "d.getX() = " << d.getX() << endl; //uncommented
cout << "d.A::getX() = " << d.A::getX() << endl; //uncommented
...

Lets jump to creation of d:

Create d(2,3): 
A::A() C::C(2) B::B(3) D::D(2, 3) 

You can see, A is created with default constructor ignoring parameters passed from constructors of B and C. As ambiguity is gone, all calls to getX() return the same value:

d.getX() = 42
d.A::getX() = 42
d.B::getX() = 42
d.C::getX() = 42

But what if we want to call parametrized constructor for A? It can be done by explicitly calling it from constructor of D:

D(int x, int y, int z): A(x), C(y), B(z)

Normally, class can explicitly use constructors of direct parents only, but there is an exclusion for virtual inheritance case. Discovering this rule "clicked" for me and helped understanding virtual interfaces a lot:

Code class B: virtual A means, that any class inherited from B is now responsible for creating A by itself, since B isn't going to do it automatically.

With this statement in mind it's easy to answer all questions I had:

  1. During D creation neither B nor C is responsible for parameters of A, it's totally up to D only.
  2. C will delegate creation of A to D, but B will create its own instance of A thus bringing diamond problem back
  3. Defining base class parameters in grandchild class rather than direct child isn't a good practice, so it should be tolerated when diamond problem exists and this measure is unavoidable.
深者入戏 2024-09-05 02:42:57

派生类的实例存储其基类的成员

没有虚拟继承,内存布局看起来像(注意D类中A成员的两个副本) :

class A: [A members]
class B: public A [A members|B members]
class C: public A [A members|C members]
class D: public B, public C [A members|B members|A members|C members|D members]

使用虚拟继承,内存布局看起来像(注意D类中A成员的单个副本):

class A: [A members]
class B: virtual public A [B members|A members]
                           |         ^
                           v         |
                         virtual table B

class C: virtual public A [C members|A members]
                           |         ^
                           v         |
                         virtual table C

class D: public B, public C [B members|C members|D members|A members]
                             |         |                   ^
                             v         v                   |
                           virtual table D ----------------|

对于每个派生类,编译器都会创建一个虚拟表,其中保存指向派生类中存储的虚拟基类成员的指针,并在派生类中添加一个指向该虚拟表的指针。

Instances of derived classes store the members of their base classes.

Without virtual inheritance, the memory layouts look like (note the two copies of the A members in class D):

class A: [A members]
class B: public A [A members|B members]
class C: public A [A members|C members]
class D: public B, public C [A members|B members|A members|C members|D members]

With virtual inheritance, the memory layouts look like (note the single copy of the A members in class D):

class A: [A members]
class B: virtual public A [B members|A members]
                           |         ^
                           v         |
                         virtual table B

class C: virtual public A [C members|A members]
                           |         ^
                           v         |
                         virtual table C

class D: public B, public C [B members|C members|D members|A members]
                             |         |                   ^
                             v         v                   |
                           virtual table D ----------------|

For each derived class, the compiler creates a virtual table holding pointers to the members of its virtual base classes stored in the derived class, and adds a pointer to that virtual table in the derived class.

做个ˇ局外人 2024-09-05 02:42:57

问题不在于编译器必须遵循的路径。问题在于该路径的端点:转换的结果。当涉及到类型转换时,路径并不重要,重要的是最终结果。

如果使用普通继承,每个路径都有自己独特的端点,这意味着强制转换的结果是不明确的,这就是问题所在。

如果使用虚拟继承,则会得到菱形层次结构:两条路径都通向同一端点。在这种情况下,选择路径的问题不再存在(或者更准确地说,不再重要),因为两条路径都会导致相同的结果。结果不再含糊不清——这才是重要的。确切的路径没有。

The problem is not the path the compiler must follow. The problem is the endpoint of that path: the result of the cast. When it comes to type conversions, the path does not matter, only the final result does.

If you use ordinary inheritance, each path has its own distinctive endpoint, meaning that the result of the cast is ambiguous, which is the problem.

If you use virtual inheritance, you get a diamond-shaped hierarchy: both paths leads to the same endpoint. In this case the problem of choosing the path no longer exists (or, more precisely, no longer matters), because both paths lead to the same result. The result is no longer ambiguous - that is what matters. The exact path doesn't.

美人如玉 2024-09-05 02:42:57

实际上,示例应该如下所示:

#include <iostream>

//THE DIAMOND PROBLEM SOLVED!!!
class A                     { public: virtual ~A(){ } virtual void eat(){ std::cout<<"EAT=>A";} }; 
class B: virtual public A   { public: virtual ~B(){ } virtual void eat(){ std::cout<<"EAT=>B";} }; 
class C: virtual public A   { public: virtual ~C(){ } virtual void eat(){ std::cout<<"EAT=>C";} }; 
class D: public         B,C { public: virtual ~D(){ } virtual void eat(){ std::cout<<"EAT=>D";} }; 

int main(int argc, char ** argv){
    A *a = new D(); 
    a->eat(); 
    delete a;
}

... 这样输出将是正确的:“EAT=>D”

虚拟继承仅解决了祖父的重复问题!
但您仍然需要将方法指定为虚拟,以便正确覆盖这些方法......

Actually the example should be as follows:

#include <iostream>

//THE DIAMOND PROBLEM SOLVED!!!
class A                     { public: virtual ~A(){ } virtual void eat(){ std::cout<<"EAT=>A";} }; 
class B: virtual public A   { public: virtual ~B(){ } virtual void eat(){ std::cout<<"EAT=>B";} }; 
class C: virtual public A   { public: virtual ~C(){ } virtual void eat(){ std::cout<<"EAT=>C";} }; 
class D: public         B,C { public: virtual ~D(){ } virtual void eat(){ std::cout<<"EAT=>D";} }; 

int main(int argc, char ** argv){
    A *a = new D(); 
    a->eat(); 
    delete a;
}

... that way the output is gonna be the correct one: "EAT=>D"

Virtual inheritance only solves the duplication of the grandfather!
BUT you still need to specify the methods to be virtual in order to get the methods correctly overrided...

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文