【问题标题】:Why is repeated inheritance / duplicate inheritance invalid?为什么重复继承/重复继承无效?
【发布时间】: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 或更多基础类型实例的类型时才有效。这对语言几乎没有增加复杂性,非常直观(在我看来),因为它反映了程序员已经熟悉的数组索引,并且我相信它是包罗万象的(即不会留下任何歧义)。

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

  1. 这是复杂的、武断的或离谱的。
  2. 有不需要语言修改的替代解决方案。
  3. 所需的标准/编译器更改会涉及太多的工作/工作。

我的回复:

  1. 不,不是;在我看来,这是最简单、最合乎逻辑的解决方案。
  2. 这些是解决方法,而不是解决方案。您不必创建空类或让编译器生成冗余模板变体来消除父类的歧义。而且,对于模板解决方法,我相信这将涉及生成的二进制文件中的冗余,因为编译器会生成每个变体的单独实现(例如Agent&lt;1&gt;Agent&lt;2&gt;),而这不是必需的(如果不需要,请纠正我)我错了;编译器是否足够聪明,可以知道类定义中没有使用模板参数,因此它不会为该参数的每个不同参数生成单独的代码?)。
  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];
}

【问题讨论】:

  • 因为“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


【解决方案1】:

为什么 C++ 不支持这个?

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

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

因此,无论这种继承的哲学理由如何,这都是无效的 C++ :

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

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

struct Agent {  double salary; }; 

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

如何在 C++ 中正确完成?

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

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;

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

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

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

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

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

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

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

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

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

Agent<1> *p = pspy;   

Agent&lt;1&gt; 的含义仅与DoubleAgent 的上下文相关。它不是具有自己独立定义的类型。你会如何解释:

class QuadrupleAgent : DoubleAgent, DoubleAgent {...};  
QuadrupleAgend *pmasterspy;
Agent<1> *p = pmasterspy; // Ambiguous: is it DoubleAgent<1>::Agent<1> or DoubleAgent<2>::Agent<1> ? 

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

【讨论】:

  • 对。 C++ 已经让你做 OP 想要的。它只要求您通过您自己继承的“传递”类显式命名它们来消除对多重继承基类的访问的歧义。
  • 我想我会争辩说你不应该创建“通过”类来从一个基类乘以继承;这应该可以通过内置语法直接实现。
  • 语言结构之间应该有内在的一致性。解决该问题所需的是提供一个别名(例如“公共代理作为 AgntOfNorth...”)。但是所有其他给出别名的构造都需要一个不同的语句。在这方面,使用传递类使用内置语法并且是一致的,不会危及现有代码或以模糊的方式重用其他现有语法,例如 或 [1] 或 {1} 或其他任何已经具有不同语义的语法
  • @bgoldst 这不是争论,只是偏好。我敢说大多数其他人有不同的偏好——当现有语法运行良好时,语言不会因特殊语法而臃肿来处理这种罕见的情况。还有一长串其他“内置语法”被提出来,它们的价值要大得多。
【解决方案2】:

我不同意这种说法“......因为没有办法限定使用哪个超类”。

当他们说“没有办法限定使用哪个超类”时,他们的意思是“如果不发明额外的语法,就无法限定使用哪个超类”。您通过假装它是一个数组来“限定”它的方式就是这样一种发明的语法。一个人可以发明无数看起来不像其他句法结构的其他句法,例如

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

我的语法看起来确实很随意,但你的也是。

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

【讨论】:

  • 感谢您的回答,但是:语言已经很复杂了,不是吗?我认为这不会增加任何显着的复杂性。我重申我认为Animal[1]::method() 的语法非常简单和合乎逻辑(没有冒犯,但它比Animal@::method() 语法更合乎逻辑)。为什么你会形容它令人发指?这些通过使语言更复杂来解决这个问题的“不那么离谱”的方法是什么?
  • @bgoldst 语言已经很复杂这一事实并不能增加它的复杂性。语法[1] 产生的问题多于解决方案。你会允许索引是一个非常量表达式吗?如果索引超出范围会发生什么?如果Animal 有虚函数,你会怎么做?你将如何从Dog 的成员函数访问Animal 的成员变量?您要解决的用例是什么?中间再引入一个类就不能解决吗?
  • 回答您的问题:1.没有。2.编译错误。 3. 如果多个不同的基类具有相同的虚函数,你会做同样的事情。 4. 与从派生类方法访问不同基类成员变量的方式相同。 5. 我没有现实生活中的用例,但我认为我的DoubleAgent 示例是一个合理的示例,说明它如何有意义。 6. 是的,但这将是一种解决方法,而不是缺乏对这一潜在有用功能的支持的理由。
  • @bgoldst 您的DoubleAgent 示例缺少暗示您希望它如何有意义的功能。当您向一种语言添加一个特性时,仅仅说它“可能有用”并不足以证明多个实现符合标准的 C++ 编译器的人的努力是正确的。你需要一个非常有说服力的理由来做这件事。您需要确保该功能值得付出努力,因为努力不是免费的 (take a look at this post by a guy from MS compiler team)。
  • @bgoldst “我认为我的 DoubleAgent 示例是一个合理的示例,说明了它的意义。” ——你想错了。 拥有一个代理和一个代理是有意义且有用的......它们是不同的代理。 成为代理和代理是没有意义的,也是无用的。如果目标是继承 Agent 成员的两个副本,那么你做错了——使用组合。 “这不是缺乏对这一潜在有用功能的支持的理由。” -- 理由是用这种废话来夸大 C++ 是愚蠢的。
【解决方案3】:

编译器不允许从一个以上的类直接继承的原因是因为 C++ 标准是这样说的。

标准这么说的原因是多种因素的结合。

首先,这在技术上不是必需的,因为您的“可能的解决方案”表明可以相对容易地实现您想要的效果,只是不那么直接。

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

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 次呢?并发症的清单还在继续。这些规则需要对程序员有意义,编译器需要比它们更复杂才能理解新的语法和语义。即使“超过两次”是被禁止的。

对于一个很少有人真正需要并且(如您所展示的)有可行替代方案的功能来说,所有这些复杂的东西。

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

【讨论】:

    【解决方案4】:

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

    双重代理有两个代理机构。我们称它们为 Agent1 和 Agent2。假设每个代理都有一个名为忠诚度的成员。如果执行以下代码:

    忠诚度 = "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 会模棱两可。

    【讨论】:

    • 代码是有效的,但是Agent中有loyality,虚拟继承在DoubleAgent中只有一个loyality。因此,两个提到的忠诚度指的是同一个忠诚度。因此,最后一个值“BadGuys”仍将是最后一个值。
    • 好点,克里斯托夫。我将编辑代码以避免误导。
    猜你喜欢
    • 2012-06-02
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-04-15
    • 2011-02-21
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多