多重继承+虚函数混乱

19

我有一个类似于这样的菱形多重继承场景:

    A
  /   \
 B     C
  \   /
    D

共同的父类A定义了一个虚函数fn()。
B和C是否都可以定义fn()
如果是这样,那么下一个问题是——D能否在不加区分地访问B和C的fn()?我假设有一些语法可以实现这个功能..
而且,D是否可以在不知道具体是谁B和C的情况下这样做?B和C可以被其他类替换,并且我希望D中的代码是通用的。

我的目标是让D以某种方式枚举它在继承链上所有实例的fn()。除了使用虚函数之外,还有其他方法可以实现吗?

6个回答

21

除非您在 D 中再次覆盖 fn,否则不可能。因为一个 D 对象中没有最终的覆盖者: CB 都覆盖了 A::fn。您有以下几个选项:

  • 删除 C::fnB::fn 中的一个。然后仍然覆盖 A::fn 的那个将成为最终的覆盖者。
  • D 中放置最终的覆盖者。然后,这个覆盖者将覆盖 A::fn 以及 CB 中的 fn

例如,以下内容会导致编译时错误:

#include <iostream>

class A {
public:
    virtual void fn() { }
};

class B : public virtual A {
public:
    virtual void fn() { }
};

class C : public virtual A {
public:
    virtual void fn() { }
};

// does not override fn!!
class D : public B, public C {
public:
    virtual void doit() {
        B::fn();
        C::fn();
    }
};

int main(int argc, char **argv) {
  D d;
  d.doit();
  return 0;
}
你可以在C和B中从A派生出非虚拟类,但这样就不再有菱形继承了。也就是说,在D对象中,每个A数据成员在B和C中都出现两次,因为你有两个A基类子对象。我建议您重新考虑设计。尽量消除需要虚拟继承的这种重复对象。它经常会导致此类冲突情况。
一个非常类似于此的情况是当您想要覆盖特定函数时。想象一下,在B和C中有同名的虚函数(现在没有共同的基类A)。在D中,你想覆盖每个函数,但给予不同的行为。根据你使用B指针或C指针调用函数,你可以得到不同的行为。Herb Sutter的多重继承第三部分描述了一种很好的方法来解决这个问题。它可能会帮助你决定你的设计。

我认为这不是shoosh想要做的。他说:“我想做的是让D以某种方式枚举其祖先中所有fn()的实例。”他不想在类D中覆盖fn()。 - vividos
3
这就是我说它不可能的原因。如果 C 和 B 都覆盖了 A::fn 方法,那么他就无法在 D 中定义 fn 而不覆盖它。 - Johannes Schaub - litb
你说的“Drop”是什么意思? - yanpas

5

首先,是的,B和C可以将fn()定义为虚函数。 其次,D当然可以通过使用作用域运算符::访问B::fn()C::fn()。 第三个问题:D至少必须知道B和C,因为您必须在继承列表中定义它们。您可以使用模板使B和C的类型开放:

class A
{
public:
   virtual ~A() {}
   virtual void fn() = 0;
};

class B: public A
{
public:
   virtual ~B() {}
   virtual void fn(){ std::cout << "B::fn()" << std::endl; }
};

class C: public A
{
public:
   virtual ~C() {}
   virtual void fn(){ std::cout << "C::fn()" << std::endl; }
};

template <typename TypeB, typename TypeC>
class D: public TypeB, public TypeC
{
public:
   void Do()
   {
      static_cast<TypeB*>(this)->fn();
      static_cast<TypeC*>(this)->fn();
   }
};

typedef D<B, C> DInst;

DInst d;
d.Do();

关于自动枚举D继承的所有类的所有fn()函数的愿望:我不确定是否可以在不使用MPL的情况下实现。至少您可以扩展上面的示例,以处理3个或更多模板参数的版本,但我猜测有一个类模板参数数量的上限(内部编译器限制)。

2
假设您有以下代码:A *a = some_d_object; a->fn(); 那么将调用哪个版本的 fn 函数? - Johannes Schaub - litb
1
@j_random_hacker 这里没有虚拟基类。 - curiousguy
1
@JohannesSchaub-litb "现在C和B都覆盖了A的函数。 "你读到一个实际上不存在的virtual关键字了吗?;) - curiousguy
2
@j_random_hacker "我错在哪里了" 你假设 D 中只有一个基类 A(尝试 A& = d;),因此只有一个成员函数 A::fn()(尝试 d.A::fn();)。实际上,D 有两个基类 A(通过检测它们的地址来检测 c|dtors),所以 d 中有两个成员函数 A::fn()d.B::A::fn()d.C::A::fn()。它们中的每一个确实都有一个最终覆盖函数。 - curiousguy
1
从N3242中,@j_random_hacker:“在派生类中,如果基类子对象的虚成员函数有多个最终覆盖程序将是不正确的。” - curiousguy
显示剩余24条评论

2
如果您真的需要追踪祖先并枚举类型,您可能需要查看Loki TypeLists。我不确定您所要求的是否真的可能没有大量的工作。请确保您在这里没有过度设计。
稍微说一下,如果您要以这种方式使用MI(即“可怕的钻石”),那么您应该非常明确地说明您想要哪个虚成员。我无法想出一个好的情况,在这种情况下,您想要选择B :: fn()的语义而不是C :: fn(),而不明确地在编写D时做出决策。您可能会根据各个方法的实际操作选择其中之一(甚至两者都选择)。一旦您做出了决定,要求是继承的更改不会改变期望或语义接口。
如果您真的担心要替换新类,例如将E替换为B,其中E不是从B派生但提供相同的接口,则应该使用模板方法,尽管我不确定为什么会有一个static_cast<>...
struct A {
    virtual ~A() {}
    virtual void f() = 0;
};
struct B: A {
    virtual void f() { std::cout << "B::f()" << std::endl; }
};
struct C: A {
    virtual void f() { std::cout << "C::f()" << std::endl; }
};

template <typename Base1, typename Base2>
struct D: Base1, Base2 {
    void g() { Base1::f(); Base2::f(); }
};

int main() {
    D<B,C> d1;
    D<C,B> d2;
    d1.g();
    d2.g();
    return 0;
}

// Outputs:
//   B::f()
//   C::f()
//   C::f()
//   B::f()

“工作正常,看起来有点更易于观看。”

2

您无法在继承中枚举fn()的定义。 C ++缺乏反射。 我能想象的唯一方法是使用巨大的循环测试所有可能祖先的typeid。 而且,想象起来会很痛苦。


0

Vividos已经回答了帖子的主要部分。 即使我使用作用域运算符而不是更繁琐的static_cast<> +解引用运算符。

根据手头的任务,也许您可以将继承关系从D更改为B和C,以获得较少的耦合组合(可能还包括从A继承)。 这是假设您不需要将D多态地用作B或C,并且您确实不需要B和C共享相同的基本实例。

如果您选择组合,则可以将B和C作为类型A的引用/指针作为参数传递给构造函数,使D完全不知道类型B和C。 在那一点上,您可以使用容器来保存尽可能多的A派生对象。 您自己的fn()实现(如果您决定这样做)或任何其他方法。


-1

我找不到一个回答这个具体问题的问题。你能找到吗? - shoosh
1
这并不是因为它涉及到多重继承,你就可以猜测它已经在其他帖子中得到了解决。他问道:“我想做的是让D以某种方式枚举其祖先中所有具有fn()实例的情况。除了虚函数之外,还有其他可能吗?”尽管我认为这是一个有点天真的问题,但你所链接的所有问题都没有谈论过这样的事情。我认为他的问题非常具体和独特。-1。 - Gui Prá

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