这个函数调用真的含糊不清吗?

4

我正在学习多重继承和钻石问题,当我从最终派生类中调用函数时,Visual Studio告诉我该调用是不明确的:

struct A
{
    virtual void aFunction() { cout << "I am A\n"; }
};
struct B : A {};
struct C : A {};
struct D : B, C {};

int main()
{
   D DObj;
   DObj.aFunction();             //  This is an ambiguous call
}

我理解如果在B和C类中覆盖了基类函数,则调用将产生歧义,但是在B和C中的“aFunction()”不是相同的吗?
此外,使B和C虚拟继承A会使错误消失。但是,我对继承时关键字“virtual”的理解是它可以防止更深层次的“派生类”从上面继承多个Base的副本 。在继承中,可以继承多个成员变量的副本,但只能继承一个具有相同名称的函数的副本。因此,例如,我可以有5个Derived类,每个类都从Base派生,然后MostDerivedClass从所有5个Derived类继承,在MostDerivedClass中,我将拥有5个Base类“成员变量”的副本,但只有一个具有相同名称的函数的副本。
换句话说,继承时的“虚拟”关键字应防止多个Base“成员变量”副本。我不明白为什么在这种情况下它会消除对含糊不清的函数调用。
编辑:谢谢,现在我慢慢理解了。对于我来说,很难想象D中的“A的两个副本”,因为A是空(没有大小)。但是我记得,C ++永远不会创建空类,在我的设置中,例如,空类的大小为1。然后我能够想象D中有“A的两个副本”,现在开始有意义了。

我认为如果没有虚函数,你会有两个完全相同的函数副本(尽管这仍然是我自己需要研究的东西)。 - Paul Stelian
4
没错,D 包含两个 A 的实例。编译器不知道您是要调用 D::B::A::aFunction 还是 D::C::A::aFunction。虽然在任一情况下调用的函数都相同,但不同的调用将获得不同的 this 指针。如果您将从 A 派生的类声明为 virtual,则只会有一个 A,没有歧义。struct B : virtual A {};struct C : virtual A {}; - Waxrat
而且(只是为了证明这不是Visual C++编译器的问题),GCC 5.1也会出现相同的错误。 - UnholySheep
1
这个函数调用真的有歧义吗?如果一个被全球数千人使用的编译器在使用你的示例代码时出现了如此简单的错误,你肯定会知道的,相信我。 - PaulMcKenzie
2
你遇到了所谓的“恐怖钻石”问题。这里有一个好的提示可以帮助你解决它。 - lakeweb
假设A有一个成员变量,而aFunction将其打印出来。 - M.M
3个回答

5
由于调用中存在两个可能的 `A` 基对象可作为调用的 `this` 参数,因此该调用是不明确的。即使最终调用的是相同的物理函数,并且该函数完全忽略其 `this` 参数,但存在两个 `this` 参数使得调用变得不确定。
使用虚拟继承意味着只有一个 `A` 基对象,这样调用就不会出现歧义了。

2
由于成员变量会被继承多次,所以您可能会拥有两个具有不同行为的函数的独立副本。
struct A
{
    int x;
    virtual void aFunction() { cout << "I am A with value " << x ; }
};
struct B : A {
};
struct C : A {
};
struct D : B, C {};

int main()
{
   D DObj;
   ((B*)(&DObj))->x = 0; // set the x in B to 0
   ((C*)(&DObj))->x = 1; // set the x in C to 1
   DObj.aFunction();             //  This is an ambiguous call
}

这个输出应该是0还是1?

编译器可以检测内联函数的一个特殊情况,即没有引用this指针的情况,但你可以很容易地绕过此问题,因此为了一个相对不常见的情况而增加复杂性并不值得。


在你的例子中,如果D最后调用B的构造函数或A的构造函数,这会有影响吗?这很令人困惑,但是如果在构造D时它调用了B的构造函数,然后是C的构造函数,那么答案应该是1。如果以另一种方式调用它们,则答案为0? - Zebrafish
是的,我想我慢慢开始明白了。我想象一个空类的大小为1,并且有两个Base类的副本。如果Base类没有任何大小,因为它没有任何成员变量,我很难想象一个大小为零的类的两个副本。 - Zebrafish
@TitoneMaurice 为了避免这种问题,类始终至少有1个字节长。因此,即使没有成员,编译器也会添加1个字节的填充以给出类的实际大小。 - user1937198
@rici 我不知道这一点。只是好奇,有没有什么方法可以观察到基类的大小为零,而与基类填充重叠子类成员的方式有所区别? - user1937198
本质上,它是合法的,因为它是不可检测的:) 在你可以看到正在发生的情况下,它是不合法的(例如,如果派生类的第一个成员与空基类具有相同的类型)。请参见http://en.cppreference.com/w/cpp/language/ebo进行一些解释,以及http://coliru.stacked-crooked.com/a/6ea10fc8b8f6c00f进行有趣的实用示例(元组使用继承来精确地允许这种可能性)。 - rici
显示剩余5条评论

0
使用虚拟继承来解决菱形继承问题:
struct A
{
    int x;
    virtual void aFunction() { cout << "I am A with value " << x ; }
};
struct B : virtual A { // add virtual
};
struct C : virtual A { // virtual
};
struct D : B, C {};

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接