为什么C ++不允许基类实现派生类的继承接口?

时间:2012-05-05 17:34:47

标签: c++ class inheritance interface

以下是我所说的

// some guy wrote this, used as a Policy with templates
struct MyWriter {
  void write(std::vector<char> const& data) {
    // ...
  }
};

在一些现有代码中,人们不使用模板,而是使用接口+类型擦除

class IWriter {
public:
  virtual ~IWriter() {}

public:
  virtual void write(std::vector<char> const& data) = 0;
};

其他人想要同时使用这两种方法和写作

class MyOwnClass: private MyWriter, public IWriter {
  // other stuff
};

MyOwnClass是MyWriter实现的。为什么MyOwnClass的继承成员函数不能自动实现IWriter的接口?相反,用户必须编写转发函数,除了调用基类版本之外什么都不做,如

class MyOwnClass: private MyWriter, public IWriter {
public:
  void write(std::vector<char> const& data) {
    MyWriter::write(data);
  }
};

我知道在Java中,当你有一个实现接口的类并从一个碰巧有合适方法的类派生时,该基类会自动实现派生类的接口。

为什么C ++不这样做?这似乎很自然。

5 个答案:

答案 0 :(得分:15)

这是多重继承,有两个具有相同签名的继承函数,都具有实现。这就是C ++与Java不同的地方。

因此,对静态类型为write的表达式调用MyBigClass对于所需的继承函数是不明确的。

如果只通过基类指针调用write,则不需要在派生类中定义write,这与问题中的声明相反。现在,问题改为包含一个纯说明符,在派生类中实现该函数是使类具体和可实例化的必要条件。

MyWriter::write不能用于MyBigClass的虚拟调用机制,因为虚拟调用机制需要一个接受隐式IWriter* const this的函数,MyWriter::write接受隐式MyWriter* const this。需要一个新函数,该函数必须考虑IWriter子对象和MyWriter子对象之间的地址差异。

理论上编译器可以自动创建这个新函数,但它很脆弱,因为基类的更改可能会突然导致选择新函数进行转发。它在Java中不那么脆弱,只有单一继承是可能的(转发的函数只有一个选择),但在支持完全多重继承的C ++中,选择是模糊的,我们甚至没有开始钻石继承或虚拟继承。

实际上,这个问题(子对象地址之间的差异)是为虚拟继承而解决的。但它需要额外的开销,这在大多数时候都是不必要的,而C ++的指导原则是“你不为你不使用的东西买单”。

答案 1 :(得分:5)

  

为什么C ++不这样做?这似乎很自然。

实际上,不,这是非常不自然的事情。

请注意,我的推理是基于我对“常识”的理解,因此可能会产生根本性的缺陷。

你知道,你有两种不同的方法,第一种是MyWriter,非虚拟,第二种是IWriter,是虚拟的。尽管“看起来”相似但它们完全不同。

我建议查看this question。非虚方法的好处在于,无论你做什么,只要它们不调用虚方法,它们的行为就永远不会改变。即使用非虚方法从您的类派生的人不会通过屏蔽它们来破坏现有方法。虚拟方法旨在被覆盖。这样做的代价是可以通过不正确地覆盖虚拟方法来破坏底层逻辑。这是你问题的根源。

让我们说你的建议是允许的。 (通过多重继承自动转换为虚拟)有两种可能的解决方案:

解决方案#1 MyWriter变得虚拟化。后果:世界上所有现有的C ++代码都很容易通过拼写错误或名称冲突来破解。 MyWriter方法最初不应该被覆盖,因此当有人派生自MyOwnClass时,突然将其转换为虚拟意志(墨菲定律)会破坏MyWriter类的基础逻辑。这意味着突然制作MyWriter :: write virtual是一个坏主意。

Soluion#2 MyWriter保持静态BUUUT它暂时作为虚拟方法包含在IWriter中,直到覆盖。乍一看没有什么可担心的,但让我们考虑一下。 IWriter实现了你想到的某种概念,它应该做一些事情。 MyWriter实现了另一个概念。要将MyWriter :: write指定为IWriter :: write方法,您需要两个保证:

  1. 编译器必须确保MyWriter :: write执行IWriter :: write()应该执行的操作。
  2. 编译器必须确保从IWriter调用MyWriter :: write不会破坏程序员希望在别处使用的MyWriter代码中的现有功能。
  3. 所以,问题是编译器无法保证。函数具有相似的名称和参数列表,但是根据墨菲定律,这意味着它们可能做了完全不同的事情。 (例如,sinf和cosf具有相同的参数列表),编译器不太可能预测未来,并确保在开发过程中不会以任何方式更改MyWriter,使其与IWriter不兼容。因此,由于机器本身无法做出合理的决定(没有AI),它必须问你,程序员 - “你想做什么?”。你说“将虚方法重定向到MyWriter :: write()。它完全不会破坏任何东西。我想。”

    这就是为什么你必须指定你想要手动使用哪种方法....

答案 2 :(得分:4)

自动执行 会不直观且令人惊讶。 C ++不假设多个基类彼此相关,并通过为非静态成员定义嵌套名称说明符来保护用户免受其成员之间的名称冲突。将隐式声明添加到MyOwnClass来自IWriterMyWriter的签名冲突将与保护名称相对立。

然而,C ++ 11扩展确实让我们更接近。考虑一下:

class MyOwnClass: private MyWriter, public IWriter {
public:
  void write(std::vector<char> const& data) final = MyWriter::write;
};

这种机制是安全的,因为它表示MyWriter不期望任何进一步的覆盖,并且方便,因为它命名将被“加入”的函数签名,但仅此而已。此外,如果函数不是隐式finalvirtual将是格式错误的,因此它会检查签名是否与虚拟接口匹配。

一方面,大多数接口都不会恰好以这种方式匹配。将此功能定义为仅使用相同的签名是安全的,但很少有用。将它定义为委托函数体的快捷方式将是有用的但很脆弱。所以它可能不是一个好的功能

另一方面,这是一个很好的设计模式,可以提供在不需要的时候不是虚拟的功能。因此,考虑到这个习惯用法,我们可能会用它来编写好的代码,即使它与当前的实践不匹配。

答案 3 :(得分:3)

  

为什么C ++不这样做?

我不确定你在这里问的是什么。 可以重写C ++以允许这个吗?是的,但到了什么目的?

由于MyWriterIWriter是完全不同的类,因此在C ++中通过MyWriter的实例调用IWriter的成员是非法的。成员指针具有完全不同的类型。正如MyWriter*无法转换为IWriter*一样,void (MyWriter::*)(const std::vector<char>&)也无法转换为void (IWriter::*)(const std::vector<char>&)

C ++的规则不会因为可以成为组合这两者的第三类而改变。两个班级都不是彼此的直接父母/子女亲属。因此,它们被视为完全不同的类别。

请记住:成员函数总是带有一个额外的参数:指向它们指向的对象的this指针。您无法在void (MyWriter::*)(const std::vector<char>&)上致电IWriter*。第三个类可以有一个方法将自己强制转换为正确的基类,但实际上它必须具有此方法。因此,您或C ++编译器必须创建它。 C ++的规则需要这个。

考虑在没有派生类方法的情况下使这项工作必须发生的事情。

一个函数获得IWriter*。用户只使用write指针调用IWriter*成员。那么......编译器究竟如何生成调用MyWriter::writer的代码?请注意:MyWriter::writer 需要一个MyWriter实例。 IWriterMyWriter之间没有任何关系。

那么编译器究竟如何在本地进行类型强制?编译器必须检查虚函数以查看要调用的实际函数是否采用IWriter或其他类型。如果它采用另一种类型,则必须将指针转换为其真实类型,然后再执行另一种转换为虚函数所需的类型。完成所有这些后,它就能够进行通话。

所有这些开销都会影响每个虚拟呼叫。他们所有人都必须至少检查是否要调用实际功能。每次调用都必须生成代码以进行类型转换,以防万一。

每个虚函数调用都会有一个“get type”和条件分支。即使从不可能触发该分支。无论你是否使用它,你都会付出代价。那不是C ++方式。

更糟糕的是,不再可能实现虚拟调用的直接v表实现。执行虚拟分派的最快方法不是一致的实现。 C ++委员会 将进行任何可能使这种实现无法实施的更改。

再次,到底是什么?这样你就不必编写简单的转发功能了吗?

答案 4 :(得分:-2)

只需让MyWriter派生自IWriter,消除MyOwnClass中的IWriter派生,继续生活。这应解决问题,不应干扰模板代码。