【发布时间】:2015-04-28 23:47:31
【问题描述】:
此代码无效:
struct Agent {};
struct DoubleAgent : public Agent, public Agent {};
因为:
> g++ -c DoubleAgent.cpp
DoubleAgent.cpp:2:43: error: duplicate base type ‘Agent’ invalid
struct DoubleAgent : public Agent, public Agent {};
^
为什么?
我不认为这违反了继承的概念;如果一个类可以与基类具有 is-a 关系,那么它是否应该能够与基类具有 is-two-of-a 关系班级?特别是考虑到这一点,由于 C++ 支持多重继承,一个类已经可以是多个不同的基类型。
此外,所谓的diamond problem 已经间接支持这一点,其中多个(不同的)基类可以从一个公共基类继承。
我从 Wikipedia 中找到了一些引用,似乎暗示这是 OO 语言中常见但不普遍的约束,可能只是出于对句法复杂性的恐惧,并且是不受欢迎的约束:
Wikipedia: Inheritance (object-oriented programming)#Design constraints
设计限制
在设计程序时广泛使用继承会带来一定的限制。
例如,考虑一个包含人名的 Person 类, 出生日期、地址和电话号码。我们可以定义一个子类 名为 Student 的人,其中包含此人的平均成绩 和所采用的类,以及另一个名为 Employee 的 Person 子类 包含此人的职位、雇主和薪水。
在定义这个继承层次结构时,我们已经定义了某些 限制,并非所有这些都是可取的:
- 单一性:使用单一继承,子类只能从一个超类继承。继续上面给出的例子,Person 可以是学生或员工,但不能同时是两者。使用多个 继承部分解决了这个问题,因为可以定义一个 从 Student 和 Employee 继承的 StudentEmployee 类。 但是,在大多数实现中,它仍然可以从每个 超类只有一次,因此不支持以下情况 学生有两份工作或就读于两所院校。遗产 Eiffel 中可用的模型通过支持 重复继承。
Wikipedia: Multiple Inheritance#Mitigation
C++ 不支持显式重复继承,因为无法限定使用哪个超类(即,一个类在单个派生列表中出现多次 [class Dog : public Animal, Animal])。
我不同意这种说法“......因为没有办法限定使用哪个超类”。编译器不能只允许对基本限定符进行某种索引,例如Animal[1]::method()(同样,也许允许将类定义中的重复折叠到struct DoubleAgent : public Agent[2] {};)?我认为这没有任何问题,从语法和概念上来说,这不是一个非常简单的解决方案吗?
我相信我已经找到了一些可能的解决方法,尽管我不能确定它们是否真的可以在真实系统中工作,或者它们是否会导致无法解决的问题:
1:在基类上消除模板非类型参数的歧义。
template<int N> struct Agent {};
struct DoubleAgent : public Agent<1>, public Agent<2> {};
2:消除中间基类的歧义(菱形继承)。
(注意:我不必在此处使用带有 CRTP 的模板类,但我认为如果要重复使用此模式(反模式?),这将有助于通用性和减少重复代码。)
template<typename T> struct Primary : public T {};
template<typename T> struct Secondary : public T {};
struct Agent {};
struct DoubleAgent : public Primary<Agent>, public Secondary<Agent> {};
请评论:
- 继承/多重继承/重复继承的一般概念,
- 在 C++(或其他语言)中不支持重复继承的原因,
- 在 Eiffel(或其他语言)中明显支持重复继承背后的基本原理,以及
- 我可能的解决方法如上所示。
编辑:我想强调我建议的在 C++ 中重复继承的语法,因为我觉得它的简单性和全面性被答案所忽视或低估了。
以下代码演示了这个想法,涵盖了演员阵容(@stefan 评论过)和成员资格:
struct Agent { int id; };
struct TripleAgent : public Agent[3] {};
int main() {
TripleAgent tripleAgent;
tripleAgent.Agent[0]::id = 1;
tripleAgent.Agent[1]::id = 2;
tripleAgent.Agent[2]::id = 3;
Agent* agent0 = (Agent[0]*)tripleAgent;
Agent* agent1 = (Agent[1]*)tripleAgent;
Agent* agent2 = (Agent[2]*)tripleAgent;
}
如您所见,Agent[N] 基本上本身就是一个类型名称,但仅在限定或强制转换拥有N+1 或更多基础类型实例的类型时才有效。这对语言几乎没有增加复杂性,非常直观(在我看来),因为它反映了程序员已经熟悉的数组索引,并且我相信它是包罗万象的(即不会留下任何歧义)。
回答者似乎遇到的主要困难如下:
- 这是复杂的、武断的或离谱的。
- 有不需要语言修改的替代解决方案。
- 所需的标准/编译器更改会涉及太多的工作/工作。
我的回复:
- 不,不是;在我看来,这是最简单、最合乎逻辑的解决方案。
- 这些是解决方法,而不是解决方案。您不必创建空类或让编译器生成冗余模板变体来消除父类的歧义。而且,对于模板解决方法,我相信这将涉及生成的二进制文件中的冗余,因为编译器会生成每个变体的单独实现(例如
Agent<1>和Agent<2>),而这不是必需的(如果不需要,请纠正我)我错了;编译器是否足够聪明,可以知道类定义中没有使用模板参数,因此它不会为该参数的每个不同参数生成单独的代码?)。 - 我想这是最好的论据;该功能的有用性可能无法证明为实现它而付出的努力。但这并不是反对这个想法本身的论据。当我问这个问题时,我希望对这个想法进行更理论、更学术的讨论,而不是“我们没有时间做这个”的反应。
编辑:这是另一种可能的语法,我实际上更喜欢它。它允许将超类型名称作为成员访问,它有点像:
struct Person { int height; };
struct Agent { int id; };
struct TripleAgent : public Person, public Agent[3] {};
int main() {
TripleAgent tripleAgent;
tripleAgent.Person.height = 42;
tripleAgent.Agent[0].id = 1;
tripleAgent.Agent[1].id = 2;
tripleAgent.Agent[2].id = 3;
Person& person = tripleAgent.Person;
Agent& agent0 = tripleAgent.Agent[0];
Agent& agent1 = tripleAgent.Agent[1];
Agent& agent2 = tripleAgent.Agent[2];
}
【问题讨论】:
-
因为“is-a”和“is-two-of-a”不是一回事,这是两种不同的关系,C++只对第一种有直接的支持。
-
好吧,如果这是可能的,那么您将遇到将
DoubleAgent*转换为Agent*的问题,因为这将是模棱两可的。 -
听起来您的第一个解决方案确实搞定了。它会在真实系统中工作吗?取决于你为什么/如何使用它。
-
"is-two-of-a" 听起来像 "is-a-and-has-two-roles"... 但是这是主观意见,不是客观陈述
-
typedef Agent TripleAgent[3];将进一步简化您的示例代码 ;)
标签: c++ inheritance multiple-inheritance language-lawyer