通过多重继承合并抽象和最终方法
Merging abstract and final methods through 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
,那么它与 GroundVehicle
中的 final
声明相矛盾。如果我不去实现,那么Tank
就是抽象的,无法实例化。在从 DrivableAssaultMachine
继承的上下文中,是否有其他方法可以识别 Tank
对 drive()
的继承实现?
编辑: 重命名所有内容以提供更具体的示例。
概念缺陷:
class FighterJet
无法从 DrivableAssaultMachine
派生为
DrivableAssaultMachine
派生自 class GroundVehicle
.
FighterJet
不是 GroundVehicle
。
您应该区分 GroundVehicles
和 AirVehicles
并添加: drive
和 fly
作为成员函数,然后 然后 从第一个导出 Tank
,从第二个导出 FighterJet
。
第 n 次编辑后:
您刚刚考虑了我告诉您的内容并更改了继承结构,从 WheeledVehicle
制动 DrivableAssaultMachine
最终答案:
If I implement drive()
, then it contradicts the final
declaration in GroundVehicle
.
drive()
在 FighterJet
中是多余的(因为它有 fly()
可用),从那里删除它可以让你在 Tank
.[=45 中覆盖它=]
DrivableAssaultMachine
中的virtual drive
与继承自WheeledVehicle
的drive()
无关。因此,当尝试在 Tank
中覆盖它时应该存在歧义,因为它具有作为基础 class 的 DrivableAssaultMachine
和间接 WheeledVehicle
.
注意:一般来说,你的抽象 class 应该提供基本的功能,例如成员函数:一个虚拟的 move()
然后派生的 classes 应该把它包装起来(覆盖它) 并将其专门化以满足他们的需求,例如:drive()
和 fly()
。指定可见性和可访问性的唯一工具是在定义和继承期间通过 public
、private
和 protected
说明符。
C++ 不是 Java.
C++ 中的继承和覆盖必须比 Java 中的更明确。 (我认为 C++ 就在这里。)
在 C++ 中,两个虚函数声明 foo
(具有相同的签名)被认为是 同音异义、不同的、不相关的函数,除非一个声明覆盖了另一个。
struct B1 {
virtual void foo();
};
struct B2 {
virtual void foo();
};
struct D : B1, B2 {
void bar();
};
D
有两个具有相同签名的成员函数的继承声明,在 D::bar()
中对 foo()
的不合格调用会产生歧义,但是 [=81= 的定义] D
格式正确。 在 C++ 中存在潜在的歧义没有问题,只有歧义调用是不允许的。
这意味着您可以从基础 classes 派生出不相关的同音异义词。您只需要避免模棱两可的调用即可。
即使 foo()
的两个声明来自同一个 class:
struct B {
virtual void foo();
};
struct Left : B {
};
struct Right : B {
};
struct D : Left, Right {
void bar();
};
B::foo()
的两个继承声明只是同音异义词:它们来自同一个 class,但它们在两个不同的基础 class 中。 C++ 让你拥有同音异义词。 编译器不相信这些函数做同样的事情或者除了具有相同的名称、显式参数列表(括号内的参数)、隐式 this
参数类型(类型 Base*
),并在源文件中由相同的声明声明!
当然,存在潜在的歧义:如果您尝试在派生对象上调用 foo
,或尝试转换(隐式地,或显式地使用 static_cast
或 C 风格的转换)指向不明确基数 class 指针的派生指针。
[注意:有些人称其为 "diamond inheritance"(尽管继承图是树而不是 "diamond"),或 "dreaded diamond"(如果您不理解,这只会令人恐惧C++ 中的 MI)或 "diamond of death"(谁死了?)或 "diamond of end of the world"(现在我正在编造一些东西,但它只比 "diamond of death" 稍微夸张一点)。说真的,不要吓到有潜在歧义的人。 --尾注]
这意味着您可以从基 classes 派生出同一个非虚拟基 class 的不相关派生。您只需要避免模棱两可的调用和模棱两可的转换。 (当两个 class 像 refcounted_base
一样从同一个实用程序 class 派生时,就会发生这种情况。)
在Java中,classes的多重继承不存在,但多重接口继承存在,并且给定(纯虚拟)函数的不同声明是同义词:
interface I {
void foo();
}
class Base {
public void foo() { }
}
class Der extends Base implements I {
}
此处 Base::foo()
将实施 I::foo()
,即使 Base
和 I
都没有命名或彼此不认识。这在 C++ 中永远不会发生。
Although my feeling is that the compiler should be able to resolve ambiguities and virtualization here,
您的感觉是基于 C++ 继承模型的错误概念;编译器无法知道您的意图。您给了编译器两个不同的声明,仅仅因为它们是同音异义词并不意味着您打算使用同义词。
通过从公共接口继承(使用接口应有的虚拟继承),您是在告诉编译器只有一个特性 drive
。 drive
的所有用法现在都是同义词。
我通过创建菱形层次结构并使用虚拟继承解决了这个问题。以下是问题层次结构的简化可视化:
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;
}
一般来说,解决方法似乎是在继承树的根部附近使用简单的虚拟接口,并在接近叶子时组合或复杂化接口。
考虑以下程序:
#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
,那么它与 GroundVehicle
中的 final
声明相矛盾。如果我不去实现,那么Tank
就是抽象的,无法实例化。在从 DrivableAssaultMachine
继承的上下文中,是否有其他方法可以识别 Tank
对 drive()
的继承实现?
编辑: 重命名所有内容以提供更具体的示例。
概念缺陷:
class FighterJet
无法从 DrivableAssaultMachine
派生为
DrivableAssaultMachine
派生自 class GroundVehicle
.
FighterJet
不是 GroundVehicle
。
您应该区分 GroundVehicles
和 AirVehicles
并添加: drive
和 fly
作为成员函数,然后 然后 从第一个导出 Tank
,从第二个导出 FighterJet
。
第 n 次编辑后:
您刚刚考虑了我告诉您的内容并更改了继承结构,从 WheeledVehicle
DrivableAssaultMachine
最终答案:
If I implement
drive()
, then it contradicts thefinal
declaration inGroundVehicle
.
drive()
在 FighterJet
中是多余的(因为它有 fly()
可用),从那里删除它可以让你在 Tank
.[=45 中覆盖它=]
DrivableAssaultMachine
中的virtual drive
与继承自WheeledVehicle
的drive()
无关。因此,当尝试在 Tank
中覆盖它时应该存在歧义,因为它具有作为基础 class 的 DrivableAssaultMachine
和间接 WheeledVehicle
.
注意:一般来说,你的抽象 class 应该提供基本的功能,例如成员函数:一个虚拟的 move()
然后派生的 classes 应该把它包装起来(覆盖它) 并将其专门化以满足他们的需求,例如:drive()
和 fly()
。指定可见性和可访问性的唯一工具是在定义和继承期间通过 public
、private
和 protected
说明符。
C++ 不是 Java.
C++ 中的继承和覆盖必须比 Java 中的更明确。 (我认为 C++ 就在这里。)
在 C++ 中,两个虚函数声明 foo
(具有相同的签名)被认为是 同音异义、不同的、不相关的函数,除非一个声明覆盖了另一个。
struct B1 {
virtual void foo();
};
struct B2 {
virtual void foo();
};
struct D : B1, B2 {
void bar();
};
D
有两个具有相同签名的成员函数的继承声明,在 D::bar()
中对 foo()
的不合格调用会产生歧义,但是 [=81= 的定义] D
格式正确。 在 C++ 中存在潜在的歧义没有问题,只有歧义调用是不允许的。
这意味着您可以从基础 classes 派生出不相关的同音异义词。您只需要避免模棱两可的调用即可。
即使 foo()
的两个声明来自同一个 class:
struct B {
virtual void foo();
};
struct Left : B {
};
struct Right : B {
};
struct D : Left, Right {
void bar();
};
B::foo()
的两个继承声明只是同音异义词:它们来自同一个 class,但它们在两个不同的基础 class 中。 C++ 让你拥有同音异义词。 编译器不相信这些函数做同样的事情或者除了具有相同的名称、显式参数列表(括号内的参数)、隐式 this
参数类型(类型 Base*
),并在源文件中由相同的声明声明!
当然,存在潜在的歧义:如果您尝试在派生对象上调用 foo
,或尝试转换(隐式地,或显式地使用 static_cast
或 C 风格的转换)指向不明确基数 class 指针的派生指针。
[注意:有些人称其为 "diamond inheritance"(尽管继承图是树而不是 "diamond"),或 "dreaded diamond"(如果您不理解,这只会令人恐惧C++ 中的 MI)或 "diamond of death"(谁死了?)或 "diamond of end of the world"(现在我正在编造一些东西,但它只比 "diamond of death" 稍微夸张一点)。说真的,不要吓到有潜在歧义的人。 --尾注]
这意味着您可以从基 classes 派生出同一个非虚拟基 class 的不相关派生。您只需要避免模棱两可的调用和模棱两可的转换。 (当两个 class 像 refcounted_base
一样从同一个实用程序 class 派生时,就会发生这种情况。)
在Java中,classes的多重继承不存在,但多重接口继承存在,并且给定(纯虚拟)函数的不同声明是同义词:
interface I {
void foo();
}
class Base {
public void foo() { }
}
class Der extends Base implements I {
}
此处 Base::foo()
将实施 I::foo()
,即使 Base
和 I
都没有命名或彼此不认识。这在 C++ 中永远不会发生。
Although my feeling is that the compiler should be able to resolve ambiguities and virtualization here,
您的感觉是基于 C++ 继承模型的错误概念;编译器无法知道您的意图。您给了编译器两个不同的声明,仅仅因为它们是同音异义词并不意味着您打算使用同义词。
通过从公共接口继承(使用接口应有的虚拟继承),您是在告诉编译器只有一个特性 drive
。 drive
的所有用法现在都是同义词。
我通过创建菱形层次结构并使用虚拟继承解决了这个问题。以下是问题层次结构的简化可视化:
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;
}
一般来说,解决方法似乎是在继承树的根部附近使用简单的虚拟接口,并在接近叶子时组合或复杂化接口。