考虑以下计划:
#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}}继承实现?
编辑:重命名所有内容以提供更具体的示例。
答案 0 :(得分:4)
概念缺陷:
class FighterJet
无法从DrivableAssaultMachine
派生
DrivableAssaultMachine
源自class GroundVehicle
。
FighterJet
不是GroundVehicle
s。
您应区分GroundVehicles
和AirVehicles
并添加:drive
和fly
作为成员函数,然后派生{{1}来自第一个和Tank
,来自第二个。
您刚刚考虑了我告诉您的内容并更改了继承结构,从FighterJet
DrivableAssaultMachine
如果我实施
WheeledVehicle
,那么它与drive()
中的final
声明相矛盾。
GroundVehicle
中{p> drive()
是多余的(因为它有FighterJet
可用),将其从fly()
移除将允许您在Tank
中覆盖它。
virtual drive
中的DrivableAssaultMachine
与drive()
继承的WheeledVehicle
无关。因此,尝试在Tank
中覆盖它时应该存在歧义,因为它具有DrivableAssaultMachine
和间接WheeledVehicle
的基类。
注意:通常,您的抽象类应该提供基本功能,例如成员函数:虚拟move()
,然后派生类应该将其包装起来(覆盖它)并根据需要对其进行专门化,例如: drive()
和fly()
。您定义可见性和可访问性的唯一工具是在定义和继承期间通过public
,private
和protected
说明符。
答案 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 ++继承模型的错误概念;编译器无法知道你的意图。你给了编译器两个不同的声明,只是因为它们是同音异义词并不意味着你想要同义词。
通过继承公共接口(使用接口的虚拟继承),您告诉编译器只有一个功能drive
。 drive
的所有用法现在都是同义词。