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

Why is repeated inheritance / duplicate inheritance invalid?

此代码无效:

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 {};
                                           ^

为什么?

我不认为这违反了继承的概念;如果 class 可以与基础 class 具有 is-a 关系,那么它不应该具有 is-二分之一 与基数 class 的关系?特别是考虑到,由于 C++ 支持多重继承,一个 class 已经可以 多个不同的基类型。

此外,这已经得到了所谓的 diamond problem 的间接支持,其中多个(不同的)基 classes 可以从公共基 class 继承。

我从维基百科中发现了一些引述,似乎暗示这是 OO 语言中常见但不普遍的约束,可能只是由于担心句法复杂性的结果,并且是不受欢迎的约束:


Wikipedia: Inheritance (object-oriented programming)#Design constraints

Design constraints

Using inheritance extensively in designing a program imposes certain constraints.

For example, consider a class Person that contains a person's name, date of birth, address and phone number. We can define a subclass of Person called Student that contains the person's grade point average and classes taken, and another subclass of Person called Employee that contains the person's job-title, employer, and salary.

In defining this inheritance hierarchy we have already defined certain restrictions, not all of which are desirable:

  • Singleness: using single inheritance, a subclass can inherit from only one superclass. Continuing the example given above, Person can be either a Student or an Employee, but not both. Using multiple inheritance partially solves this problem, as one can then define a StudentEmployee class that inherits from both Student and Employee. However, in most implementations, it can still inherit from each superclass only once, and thus, does not support cases in which a student has two jobs or attends two institutions. The inheritance model available in Eiffel makes this possible through support for repeated inheritance.

Wikipedia: Multiple Inheritance#Mitigation

C++ does not support explicit repeated inheritance since there would be no way to qualify which superclass to use (i.e. having a class appear more than once in a single derivation list [class Dog : public Animal, Animal]).


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

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

1: 消除基于 class.

的模板非类型参数的歧义
template<int N> struct Agent {};
struct DoubleAgent : public Agent<1>, public Agent<2> {};

2:消除中间基 classes(钻石继承)的歧义。

(注意:我不必在此处将模板 classes 与 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++ 重复继承语法,因为我觉得答案忽略或低估了它的简单性和全面性。

下面的代码演示了这个想法,涵盖了转换(由@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 或更多基础类型实例的类型时才有效。这给语言增加了很少的复杂性,非常直观(在我看来),因为它反映了程序员已经熟悉的数组索引,而且我相信它是包罗万象的(即不会留下任何歧义)。

回答者似乎遇到的主要困难如下:

  1. 它是复杂的、任意的或离谱的。
  2. 有不需要修改语言的替代解决方案。
  3. 所需的 standard/compiler 更改涉及太多 effort/work。

我的回复:

  1. 不,不是;在我看来,这是最简单、最合乎逻辑的解决方案。
  2. 这些是解决方法,不是解决方案。您不必创建空 class 或让编译器生成冗余模板变体来消除父 class 的歧义。而且,对于模板解决方法,我认为这会涉及生成的二进制文件中的冗余,因为编译器会为每个变体生成单独的实现(例如 Agent<1>Agent<2>),而这不是必需的(如果我错了,请纠正我;编译器是否足够聪明,知道 class 定义中未使用模板参数,因此它不会为该参数的每个不同参数生成单独的代码?)。
  3. 我想这是最好的论据;该功能的有用性可能无法证明实现它的努力是值得的。但这并不是反对这个想法本身的论点。当我问这个问题时,我希望对这个想法进行更理论化、更学术化的讨论,而不是 "we don't have time for this" 反应。

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

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];
}

I disagree with that statement "... since there would be no way to qualify which superclass to use".

当他们说 "there would be no way to qualify which superclass to use" 时,他们的意思是 "there would be no way to qualify which superclass to use without inventing an additional syntax"。您通过假装它是一个数组来 "qualifying" 它的方式就是这样一种发明的语法;人们可以发明无数其他看起来不像其他句法结构的语法,例如

Animal@::method()  // @ means "first"
Animal@@::method() // @@ means "second"

我的语法看起来完全是随意的,但你的也是。

当然,可以通过使语言更加复杂来解决此问题的其他不那么离谱的方法。然而,关键问题是,通过使语言更复杂,您可以获得什么好处?这个问题没有明显的答案,因为您可以通过 组合 潜在地实现双重继承的几乎所有好处,而无需对语言进行任何更改。

因为你不能谈论特定的基础class。

双面间谍有两个特工体。我们称它们为 Agent1 和 Agent2。假设每个 Agent 都有一个名为 loyality 的成员。如果执行以下代码:

忠诚度="GoodGuys";

这会改变 Agent1.loyality 还是 Agent2.loyality。

以下是合法的:

class Agent{... std::string loyality;};
class PrimaryRole : public Agent {};
class SecondaryRole : public Agent{};

class DoubleAgent : public PrimaryRole, public SecondaryRole
{
   void setLoyalities()
   {
       PrimaryRole::loyality = "GoodGuys";
       SecondaryRole::loyality = "BadGuys";
   }
};

语言律师注意事项: 这也是合法的:

class DoubleAgent : public Agent, public SecondaryRole {...};

但相当无用,因为提及 Agent::loyality 会产生歧义。

为什么 C++ 不支持它?

你的编译器不接受这种多重继承的原因是因为它在 C++ 标准中被明确禁止:

Section 10.1 point 3: A class shall not be specified as a direct base class of a derived class more than once. [ Note: A class can be an indirect base class more than once and can be a direct and an indirect base class.(...) ]

所以不管这种继承的哲学理由如何,这在 C++ 中是无效的:

struct DoubleAgent : public Agent, public Agent {};  // direct base class more than once !!!

原因纯粹是语法上的。假设您有以下代理定义:

struct Agent {  double salary; }; 

双重继承是否有效,您将无法消除您想要访问的两个继承工资中的哪一个的歧义。

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

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

struct AgentFromSouth : public Agent {}; 
struct AgentFromNorth : public Agent {}; 
struct DoubleAgent : public AgentFromSouth, public AgentFromNorth {};  // valid

消歧如下:

DoubleAgent spy; 
spy.AgentFromSouth::salary = 10000.0; 
spy.AgentFromNorth::salary = 800.0;

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

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

从技术上讲,对于现代编译器,这样一个空 class 也不会产生显着的代码开销(我的编译器生成的用于通过空 class 显示继承的汇编代码完全相同与直接继承一样。)

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

您提出的基于模板的解决方案当然是一个想法。但是,这两个提议的替代方案只是语法糖,为相同的基础 class 提供两个不同的名称(Agent<1>Primary<Agent>)。这正是通过创建一个中间的空“别名”class,以今天的标准来实现的。但是问题更多。

事实上,这样的语法会导致依赖性问题。虽然对于所有 classes、前向声明规则和 class 可见性都已为现有构造明确定义,但您的替代方法并非完全如此。

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

DoubleAgent *pspy = ....; 
Agent *p = pspy;  //  ambiguous:  which role should I use ?  Agent<1> or Agent<2>  ? 

现在你肯定会说我可以很好地写出这样的东西:

Agent<1> *p = pspy;   

但是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> ? 

所有这些问题都得到了很好的处理,并且符合标准。您的短路语法会引发比它解决的问题更多的问题。那么创建这种新的和繁琐的语言结构来为现有的简单结构提供替代方案会有什么好处?

编译器不允许直接继承多个 class 的原因是因为 C++ 标准如此规定。

标准这么说的原因是综合的。

首先,这在技术上不是必需的,正如您的"possible solution"所证明的那样,可以相对轻松地实现您想要的效果,只是少一些直接。

其次,允许这种继承需要对语言语法和语义进行一系列其他更改,以便 class 的成员函数可以明确地引用多重继承基的成员。例如;

struct Agent
{
     int height;
};

struct DoubleAgent: Agent, Agent
{

};

int main()
{
      DoubleAgent a;
      a.height = 42;     //  this would be ambiguous
}

要与两个继承的 height 成员一起工作,有必要明确地引用它们。在语言规则中,这类似于

 a.Agent::height = 42;

这在当前规则下工作正常,因为不允许从 Agent 直接继承。但是如果成员直接被继承两次还是会产生歧义(指的是左边的还是右边的?)。

不知何故,要解决这个问题,有必要引入新的语法和相关规则以使语法有效。

对语言的这种修改也需要在

这样的情况下干净利落地工作
struct TripleAgent: Agent, Agent, Agent
{

};

struct QuadrupleAgent: Agent, Agent, Agent, Agent
{

};

// etc etc

毕竟,如果可以直接从同一个 struct/class 继承两次,为什么 3,4,5,0r 50 次不行呢?并发症的清单还在继续。这些规则需要对程序员有意义,而编译器需要比它们更复杂才能理解新的语法和语义。即使 "more than twice" 被禁止。

所有这些复杂的东西都是为了一个很少有人真正需要的功能,并且(如您所示)有可行的替代方案。

我只是怀疑 C++ 委员会对不必要的复杂性是否有足够的渴望来允许这样的事情。