为什么重复继承/重复继承无效?

bgo*_*dst 2 c++ inheritance multiple-inheritance language-lawyer

此代码无效:

struct Agent {};
struct DoubleAgent : public Agent, public Agent {};
Run Code Online (Sandbox Code Playgroud)

因为:

> g++ -c DoubleAgent.cpp
DoubleAgent.cpp:2:43: error: duplicate base type ‘Agent’ invalid
 struct DoubleAgent : public Agent, public Agent {};
                                           ^
Run Code Online (Sandbox Code Playgroud)

为什么?

我不认为这违反了继承的概念。如果一个类可以与基类具有is-a关系,那么它不应该与一个基类具有is-two-of-a关系吗?特别要考虑到这一点,因为C ++支持多种继承,所以一个类已经可以是多种不同的基本类型。

此外,所谓的菱形问题已经间接支持了这种情况,在该问题中,多个(不同的)基类可以从一个公共基类继承。

我在Wikipedia上找到了几句话,似乎暗示这是OO语言中的常见但非普遍性约束,可能仅是由于担心语法复杂性,并且是不受欢迎的约束:


Wikipedia:继承(面向对象编程)#Design约束

设计约束

在设计程序时广泛使用继承会施加某些约束。

例如,考虑一个Person类,其中包含一个人的姓名,出生日期,地址和电话号码。我们可以定义一个名为Student的Person子类,其中包含该人的平均成绩和所采取的课程,以及另一个名为Employee的Person子类,其中包含该人的职称,雇主和薪水。

在定义此继承层次结构时,我们已经定义了某些限制,但并非所有限制都可取:

  • 单一性:使用单一继承,子类只能从一个超类继承。继续上面给出的示例,“人”可以是“学生”或“雇员”,但不能同时是两者。使用多重继承部分地解决了这个问题,因为然后可以定义一个从Student和Employee继承的StudentEmployee类。但是,在大多数实现中,它仍然只能从每个超类继承一次,因此,不支持学生有两个工作或在两个机构就读的情况。通过支持重复继承,Eiffel中可用的继承模型使之成为可能。

维基百科:多重继承#缓解

C ++不支持显式的重复继承,因为没有办法限定要使用的超类(即,一个类在单个派生列表中出现多次(类Dog:公共动物,动物))。


我不同意该声明“ ...,因为没有办法限定要使用的超类”。编译器是否不能只允许对基本限定符进行某种索引,例如Animal[1]::method()(并且同样,也许允许将类定义中的重复折叠为struct DoubleAgent : public Agent[2] {};)?我认为没有任何问题,从语法和概念上来说这不是一个简单的解决方案吗?

我相信我已经找到了两种可能的解决方法,尽管我无法确定它们是否会在实际系统中正常工作,或者它们是否会导致无法解决的问题:

1:在基类上消除模板非类型参数的歧义。

template<int N> struct Agent {};
struct DoubleAgent : public Agent<1>, public Agent<2> {};
Run Code Online (Sandbox Code Playgroud)

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> {};
Run Code Online (Sandbox Code Playgroud)

请评论:

  • 继承/多重继承/重复继承的一般概念,
  • 不支持使用C ++(或其他语言)进行重复继承的基本原理,
  • 显然支持以埃菲尔(或其他语言)进行重复继承的理由,以及
  • 我上面显示的可能的解决方法。

编辑:我想强调我建议的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;
}
Run Code Online (Sandbox Code Playgroud)

如您所见,Agent[N]基本上就成为了一种类型名称,但仅在限定或强制转换具有N+1或更多基础类型实例的类型时才有效。这给语言增加了很少的复杂性,非常直观(在我看来),因为它反映了程序员已经熟悉的数组索引,而且我认为它是无所不包的(即不带任何歧义)。

答题者似乎面临的主要困难如下:

  1. 它是复杂的,任意的或令人发指的。
  2. 有些替代解决方案不需要修改语言。
  3. 所需的标准/编译器更改将涉及过多的工作/工作。

我的回复:

  1. 不,这不对; 我认为这是最简单,最合乎逻辑的解决方案。
  2. 这些是解决方法,而不是解决方案。您不必创建一个空的类,也不必让编译器生成一个冗余的模板变体来消除父类的歧义。并且,对于模板变通方法,我认为这将在生成的二进制文件中涉及冗余,因为在不必要的情况下,编译器将为每个变体(例如Agent<1>Agent<2>)生成单独的实现(如果我错了,请纠正我;是)编译器足够聪明,可以知道模板参数未在类定义中使用,因此它不会为该参数的每个不同参数生成单独的代码?)。
  3. 我认为这是最好的说法。功能的实用性可能无法证明实现该功能的合理性。但这不是反对该想法本身的论据。当我问这个问题时,我希望对这个想法进行更多的理论上,学术上的讨论,而不是“我们没有时间做这个”反应。

编辑:这是另一种可能的语法,我实际上更喜欢。它允许将超类型名称作为成员进行访问,其类型为:

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];
}
Run Code Online (Sandbox Code Playgroud)

Chr*_*phe 6

为什么C ++不支持此功能?

您的编译器不接受这种多重继承的原因是,在C ++标准中明确禁止使用:

10.1节第3节: 不得多次将一个类指定为派生类的直接基类。[注意:一个类可以多次成为间接基类,并且可以是直接基类和间接基类。(...)]

因此,不管这种继承的哲学依据如何,这都是无效的C ++:

struct DoubleAgent : public Agent, public Agent {};  // direct base class more than once !!!
Run Code Online (Sandbox Code Playgroud)

原因纯粹是句法上的。假设您具有以下agent定义:

struct Agent {  double salary; }; 
Run Code Online (Sandbox Code Playgroud)

如果双重继承有效,那么您将无法消除要访问的两个继承工资中的哪个歧义。

如何在C ++中正确完成此操作?

(现有的)标准解决方案是使用空类来消除基类将具有的多重角色的歧义:

struct AgentFromSouth : public Agent {}; 
struct AgentFromNorth : public Agent {}; 
struct DoubleAgent : public AgentFromSouth, public AgentFromNorth {};  // valid
Run Code Online (Sandbox Code Playgroud)

歧义消除方法如下:

DoubleAgent spy; 
spy.AgentFromSouth::salary = 10000.0; 
spy.AgentFromNorth::salary = 800.0;
Run Code Online (Sandbox Code Playgroud)

这不是解决方法! 中间类不是一个简单的解决方法。它们提供了一致的子类型语义:

  • 如果a DoubleAgent从同一个类继承两次,则该同一个类实际上具有两个不同的角色。您想使这些角色保持匿名,但是标准迫使您给它们起一个名字。因此,它有助于人类读者更好地理解代码。
  • 如果a DoubleAgent继承自(即“ is a”)AgentFromNorth,那么独立于多重继承,我应该被允许这样写: AgentFromNorth *p = &spy; 您提议的任何其他语法都需要遵守这一原则。

从技术上讲,使用现代编译器时,这样的空类也不会产生大量代码开销(由我的编译器生成的用于通过空类show继承的汇编代码与直接继承完全相同)。

关于替代方法的进一步讨论

您提出的基于模板的解决方案肯定是一个主意。但是,两个建议的替代方案都是语法糖,可以为同一基类提供两个不同的名称(Agent<1>Primary<Agent>)。这正是通过按照今天的标准创建一个中间空的“别名”类而实现的。但是还有更多的问题。

实际上,这样的语法会引起依赖性问题。尽管对于所有类,都为现有构造明确定义了前向声明规则和类可见性,但对于其他方法而言,情况并非完全如此。

例如,要定义一个指向基类的指针,我将无法编写:

DoubleAgent *pspy = ....; 
Agent *p = pspy;  //  ambiguous:  which role should I use ?  Agent<1> or Agent<2>  ? 
Run Code Online (Sandbox Code Playgroud)

现在您肯定会说我可以很好地写类似以下内容:

Agent<1> *p = pspy;   
Run Code Online (Sandbox Code Playgroud)

但是的含义Agent<1>仅与上下文有关DoubleAgent。它不是具有自己独立定义的类型。您将如何解释:

class QuadrupleAgent : DoubleAgent, DoubleAgent {...};  
QuadrupleAgend *pmasterspy;
Agent<1> *p = pmasterspy; // Ambiguous: is it DoubleAgent<1>::Agent<1> or DoubleAgent<2>::Agent<1> ? 
Run Code Online (Sandbox Code Playgroud)

标准对所有这些问题都进行了很好的处理,并且以一致的方式进行了处理。您的短路语法会提出比解决方案更多的问题。那么,创建这种新的且繁琐的语言结构为现有的简单结构提供替代方法有什么好处?

  • 对。C ++已经允许您执行OP想要的操作。它只要求您通过对您的多重继承基类的访问明确地通过继承的“通过”类来命名它们,从而消除对多重继承基类的访问的歧义。 (2认同)