13

这就是我所说的

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

在一些现有的代码中,人们没有使用模板,而是使用interfaces+type-erasure

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++ 不这样做?拥有它似乎是一件很自然的事情。

4

5 回答 5

15

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

因此,调用write静态类型为的表达式对于MyBigClass需要哪个继承函数是模棱两可的。

如果write仅通过基类指针调用,则write不需要在派生类中进行定义,这与问题中的主张相反。 现在问题已更改为包含纯说明符,因此必须在派生类中实现该函数以使该类具体且可实例化。

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

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

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

于 2012-05-05T17:46:49.257 回答
5

为什么 C++ 不这样做?拥有它似乎是一件很自然的事情。

实际上,不,拥有它是非常不自然的事情。

请注意,我的推理是基于我自己对“常识”的理解,因此可能存在根本性的缺陷。

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

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

假设您的建议是允许的。(自动转换为具有多重继承的虚拟)有两种可能的解决方案:

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

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

  1. 编译器必须确保 MyWriter::write 做 IWriter::write() 应该做的事情。
  2. 编译器必须确保从 IWriter 调用 MyWriter::write 不会破坏 MyWriter 代码程序员希望在其他地方使用的现有功能。

所以,问题是编译器不能保证这一点。函数具有相似的名称和参数列表,但根据墨菲定律,这意味着它们可能正在做完全不同的事情。(例如,sinf 和 cosf 具有相同的参数列表),并且编译器不太可能能够预测未来并确保在开发过程中不会更改 MyWriter,使其与 IWriter 不兼容。所以,由于机器不能自己做出合理的决定(没有人工智能),它必须问你,程序员——“你想做什么?”。你说“将虚拟方法重定向到 MyWriter::write()。它完全不会破坏任何东西。我认为。”。

这就是为什么您必须手动指定要使用的方法....

于 2012-05-05T22:47:45.490 回答
4

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

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

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

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

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

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

于 2012-05-06T09:20:01.263 回答
3

为什么 C++ 不这样做?

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

因为MyWriter和是完全不同的类,所以在 C++ 中通过 . 的实例IWriter调用 的成员是非法的。成员指针具有完全不同的类型。就像 a不能转换为 a 一样,a 也不能转换为a 。MyWriterIWriterMyWriter*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实例。IWriter和之间没有关系MyWriter

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

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

每个虚函数调用都会有一个“获取类型”和条件分支。即使永远不可能触发该分支。因此,无论您是否使用它,您都会为某些东西付费。这不是 C++ 的方式。

更糟糕的是,虚拟调用的直接 v-table 实现不再可能。进行虚拟分派的最快方法不是符合要求的实现。C++ 委员会不会做出任何使此类实现成为不可能的更改。

再次,为了什么目的?就为了不用写一个简单的转发函数?

于 2012-05-05T20:41:38.547 回答
-2

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

于 2012-05-05T18:51:53.253 回答