通过多重继承合并抽象和最终方法

时间:2015-09-25 12:17:03

标签: c++ abstract-class multiple-inheritance

考虑以下计划:

#include <stdio.h>
#include <memory>

using namespace std;

class WheeledVehicle {
  public:
    virtual void drive() const { printf("Default driving implementation."); }
};

class GroundVehicle : public WheeledVehicle {
  public:
    void drive() const final { WheeledVehicle::drive(); }
};

class DrivableAssaultMachine {
  public:
    virtual void drive() const = 0;
    void fire_cannon();
};

class FighterJet : public DrivableAssaultMachine {
  public:
    void fly() const { printf("flying my plane.\n"); }
    void drive() const override { fly(); }
};

class Tank : public DrivableAssaultMachine, public GroundVehicle {
};

int main(int argc, char **argv) {
  make_shared<Tank>()->drive();
  return EXIT_SUCCESS;
}

我想使用修改Tank来编译此程序。如果我实施drive,那么它与final中的GroundVehicle声明相矛盾。如果我没有实现它,那么Tank是抽象的,无法实例化。是否有其他方法可以在继承Tank的情况下识别drive()的{​​{1}}继承实现?

编辑:重命名所有内容以提供更具体的示例。

3 个答案:

答案 0 :(得分:4)

概念缺陷:

class FighterJet无法从DrivableAssaultMachine派生 DrivableAssaultMachine源自class GroundVehicle

FighterJet不是GroundVehicle s。

您应区分GroundVehiclesAirVehicles并添加:drivefly作为成员函数,然后派生{{1}来自第一个和Tank,来自第二个。

在第n个编辑之后:

您刚刚考虑了我告诉您的内容并更改了继承结构,从FighterJet

制动DrivableAssaultMachine

最终答案:

  

如果我实施WheeledVehicle,那么它与drive()中的final声明相矛盾。

GroundVehicle中{p> drive()是多余的(因为它有FighterJet可用),将其从fly()移除将允许您在Tank中覆盖它。

virtual drive中的DrivableAssaultMachinedrive()继承的WheeledVehicle无关。因此,尝试在Tank中覆盖它时应该存在歧义,因为它具有DrivableAssaultMachine和间接WheeledVehicle的基类。

注意:通常,您的抽象类应该提供基本功能,例如成员函数:虚拟move(),然后派生类应该将其包装起来(覆盖它)并根据需要对其进行专门化,例如: drive()fly()。您定义可见性和可访问性的唯一工具是在定义和继承期间通过publicprivateprotected说明符。

答案 1 :(得分:1)

我通过创建钻石层次结构并使用虚拟继承来解决这个问题。以下是我的层次结构的简化可视化问题:

                    WheeledVehicle
                                \
  DrivableAssaultMachine      GroundVehicle
                       \     /
                        Tank

虽然我的感觉是编译器应该能够在这里解决模糊和虚拟化,但事实是它无法实现。我的解决方案是完全连接钻石并使用虚拟继承:

                       Drivable
                      /       \
                     /       WheeledVehicle
                    /          /
  DrivableAssaultMachine      GroundVehicle
                       \     /
                        Tank

这是最终的计划:

#include <stdio.h>
#include <memory>

using namespace std;

class Drivable {
  public:
    virtual void drive() const = 0;
};

class WheeledVehicle : public virtual Drivable {
  public:
    virtual void drive() const override { printf("Default driving implementation.\n"); }
};

class GroundVehicle : public WheeledVehicle {
  public:
    void drive() const final { WheeledVehicle::drive(); }
};

class DrivableAssaultMachine : public virtual Drivable {
  public:
    void fire_cannon();
};

class Tank : public DrivableAssaultMachine, public GroundVehicle {
};

int main(int argc, char **argv) {
  make_shared<Tank>()->drive();
  return EXIT_SUCCESS;
}

通常,解决方法似乎是在继承树的根目录附近使用简单的虚拟接口,并在离开树叶时使用组合或复杂化接口。

答案 2 :(得分:-2)

C ++ 不是 Java。

C ++中的继承和覆盖必须比Java更明确。 (我认为C ++就在这里。)

在C ++中,两个虚函数声明foo(具有相同的签名)被认为是同音异义,不同的,无关的函数,除非一个声明覆盖另一个。

struct B1 {
    virtual void foo();
};

struct B2 {
    virtual void foo();
};

struct D : B1, B2 {
    void bar();
};

D有两个具有相同签名的成员函数的继承声明,foo()内对D::bar()的无限制调用将是不明确的,但类D的定义形成得很好。 在C ++中存在潜在的歧义没有问题,只允许不明确的调用。

这意味着您可以从具有不相关同音异义词的基类派生。你只需要避免模糊的电话。

即使foo()的两个声明都来自同一个类:

struct B {
    virtual void foo();
};

struct Left : B {
};

struct Right : B {
};

struct D : Left, Right {
    void bar();
};

B::foo()的两个继承声明都只是同音异义词:它们来自同一个类,但它们来自两个不同的基类。 C ++允许你使用同音异义词。 编译器不相信函数做同样的事情或以任何方式相关,除了具有相同的名称,显式参数列表(括号内的参数),隐式this参数类型(类型为Base*),并由源文件中的相同声明声明!

当然,存在潜在的歧义:如果您尝试在派生对象上调用foo,或尝试转换(隐式或显式地使用static_cast或C样式转换)派生指向不明确的基类指针的指针。

[注意:有些人称之为“钻石继承”(虽然继承图是一棵树而不是“钻石”),或“可怕的钻石”(如果你不理解C ++中的MI,它只是可怕的)或“死亡的钻石“(谁死了?)或”世界末日的钻石“(现在我正在制作东西,但它只比”死亡的钻石“略高一些)。说真的,不要让那些有潜在歧义的人吓坏了。 - 后注]

这意味着您可以从具有相同非虚拟基类的无关派生的基类派生。你只需要避免模糊的调用和模糊的转换。 (当两个类派生自同一个实用程序类,如refcounted_base时,就会发生这种情况。)

在Java中,不存在多个类的继承,但是多个接口继承确实存在,并且给定(纯虚拟)函数的不同声明是同义词:

interface I {
    void foo();
}

class Base {
    public void foo() { }
}

class Der extends Base implements I {
}

即使Base::foo()I::foo()都没有名字或彼此了解,Base也会实施I。这在C ++中永远不会发生。

  

虽然我的感觉是编译器应该能够在这里解决歧义和虚拟化,

你的感觉是基于C ++继承模型的错误概念;编译器无法知道你的意图。你给了编译器两个不同的声明,只是因为它们是同音异义词并不意味着你想要同义词

通过继承公共接口(使用接口的虚拟继承),您告诉编译器只有一个功能drivedrive的所有用法现在都是同义词。