【问题标题】:Why does C++ not let baseclasses implement a derived class' inherited interface?为什么 C++ 不允许基类实现派生类的继承接口?
【发布时间】:2012-05-14 21:52:59
【问题描述】:

这就是我要说的

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

【问题讨论】:

  • 因为从struct 派生与从interface 派生的意图不同?
  • 没有virtual 函数、纯规范等,这根本没有意义。停止尝试将 Java 代码剪切和粘贴到 C++ 中,并认为它会起作用。 litb 我希望比这更了解。
  • @JohannesSchaub-litb:也许其他时间。很抱歉,由于您的声誉和个人资料中的“有时我在拖钓”声明,目前我不认为您确实需要帮助(也许我弄错了)。现在我觉得这个讨论并不特别有趣。我建议检查 C++ FAQ。祝你有美好的一天。
  • @fontanini 他要求澄清。我仍然不确定你反对什么。考虑到 Johannes 或多或少地吸收了 C++ 标准,并且是该委员会或互联网上拥有最全面 C++ 知识的人之一。当有问题时,这是一个很好的问题,不会轻易被抛到一边。
  • @Konrad:当它累积了那些反对票时,这个问题要糟糕得多。

标签: c++ class inheritance interface


【解决方案1】:

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

因此,在静态类型为 MyBigClass 的表达式上调用 write 将无法确定需要哪个继承函数。

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

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

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

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

【讨论】:

  • 为什么它是否是虚拟的很重要?是什么阻止了将规则放入 C++ 标准以使案例格式正确?
  • @JohannesSchaub-litb:嗯?您的问题是“显然需要派生类中的新实现。编译器不能为我生成它吗?”还是别的什么?
  • @Johannes:不,“接口”没有“实现”。基类没有提供任何匹配void write(IMyWriter* const this, std::vector&lt;char&gt; const&amp;)的函数(使用C++风格的语法来显示调用虚函数时使用的隐式this参数的类型)。
  • "MyWriter::write 不能放入 MyBigClass 的虚拟表中,因为虚拟表需要一个接受隐式 IWriter* const this 的函数,而 MyWriter::write 接受隐式 MyWriter* const这。 ”。我不明白这一切。而且,对于基本的语言设计问题,这也是一个非常技术性的论据。 vtables 的设计需要遵循语言的设计,而不是相反。
  • @Ben 我不是要求编译器为我生成一些东西。我在问为什么现在的状态是这样的。如果编译器必须生成 MyOwnClass 来为我解决问题,那么它必须。如果有其他解决方案,那么它可以采取其他解决方案。我只是希望该类不是虚拟的,并且对 IWriter::write 的调用最终具有与 MyWriter::write 等效的行为。 &MyOwnClass::write 的精确类型是一个不同的问题 - 它可能仍然应该是 MyWriter::write 的类型(因此,如果编译器在需要时生成隐藏函数,则该函数真的隐藏)。
【解决方案2】:

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

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

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

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

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

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

解决方案 #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()。它完全不会破坏任何东西。我认为。”。

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

【讨论】:

  • 感谢您的回答。我有几个问题。 “编译器必须确保 MyWriter::write 做 IWriter::write() 应该做的事情。”。为什么是编译器而不是程序员必须确保这一点?程序员是从接口和 MyWriter 派生 MyOwnClass 的人,所以他最好检查一下 MyWriter 是否满足 IWriter。
  • 我看到了同样的事情:struct A { void nonVirtualNastiness() {} virtual void bark() { } }; 然后A 的作者决定从接口struct A : public IDoggy { virtual void bark() { } }; 派生。现在IDoggy::bark 也自动被A::bark 覆盖,编译器相信程序员检查了它们的兼容性。而nonVirtualNastiness 可能会突然通过继承自动变为虚拟,因为基类具有虚拟。
  • @JohannesSchaub-litb:“为什么是编译器而不是程序员必须确保这一点?”如果编译器要自动将 MyClass 中的方法分配为 IWriter 的虚拟实现,则需要确保这一点。如果编译器无法确保,那么编译器需要对内部程序逻辑做出假设,根据墨菲定律,任何自动假设都是不正确的。所以向程序员询问信息是有意义的。以我的经验,针对有经验用户的优秀自动化系统永远不应该对任何事情做出任何假设——它应该要求用户做出决定。 (续)
  • @JohannesSchaub-litb:(续)目前的实现程序员是确保一切按预期工作的人,程序员必须做出决定。它有点类似于 linux 发行版——“用户友好”的发行版(如 ubuntu)会假设用户并不总是知道他在做什么,并且可能会默默地选择一些操作/配置选项,这可能会让用户非常恼火。 “超级用户”分发永远不会妨碍您,永远不会为您做出任何决定,并且始终会完全按照您要求的方式执行。 (续)
  • @JohannesSchaub-litb: ..(cont) 这样的行为(完全按照你说的做)让用户可以完全控制,但代价是意外执行有害命令。据我所知,C++ 遵循“高级用户”模式,我喜欢这种方式——程序员应该是做出决定的人。当然,这是个人喜好问题。公平地说,整个事情都是品味问题,是主观意见的问题。我对语言工作很满意,并且没有看到您提到的实施方案有太多好处。
【解决方案3】:

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

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

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

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

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

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

【讨论】:

    【解决方案4】:

    为什么 C++ 不这样做?

    我不确定你在这里问什么。 是否可以重写 C++ 来实现这一点?是的,但目的是什么?

    因为MyWriterIWriter是完全不同的类,所以在C++中通过IWriter的实例调用MyWriter的成员是非法的。成员指针具有完全不同的类型。正如MyWriter* 不能转换为IWriter*void (MyWriter::*)(const std::vector&lt;char&gt;&amp;) 也不能转换为void (IWriter::*)(const std::vector&lt;char&gt;&amp;)

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

    记住:成员函数总是带有一个额外的参数:一个指向它们指向的对象的this 指针。您不能在 IWriter* 上调用 void (MyWriter::*)(const std::vector&lt;char&gt;&amp;)。第三个类可以有一个将自己转换为正确基类的方法,但它实际上必须有这个方法。因此,您或 C++ 编译器都必须创建它。 C++ 的规则要求这样做。

    考虑在没有派生类方法的情况下要使这项工作发生什么。

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

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

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

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

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

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

    【讨论】:

    • 我并不是真的要求能够通过调用IWriter::write 直接调用MyWriter::write。我只是不想将函数定义放入 MyOwnClass 中,然后转发到 MyWriter::write。编译器不能为我这样做吗?我想知道的是:结果对象可以设置为调用IWriter::write结束调用MyWriter::write。它们之间可能有蹦床和其他东西(无论需要什么让它工作),最终都会对MyWriter::write进行最终调用。
    • 在这种情况下,该语言也允许 AB 的交叉大小写,即使两个类“都不是彼此的直接父/子关系”。但是这两个类的对象都显示为单个完整对象的子对象:struct A { virtual ~A() {}}; struct B { virtual ~B() {} void f() {}}; struct C : A, B {}; int main() { A *a = new C; dynamic_cast&lt;B*&gt;(a)-&gt;f(); }。在我的例子中,这两个类都显示为单个派生类的基类。
    • @Johannes:“编译器不能为我做那件事吗?”是的,它可以。但它不会隐式执行,因此您仍然需要 some 语法来告诉编译器执行此操作。而且由于该语法需要一个函数签名,以及要调用的函数的名称(可能还有签名)......它实际上只是自己编写转发函数的一小部分语法糖。随意向 C++1y 标准化委员会提出这个建议。不过,考虑到它有多大的极端情况,我不希望它获得太大的吸引力。
    • @litb:在这种情况下,语言是否允许交叉转换?我认为这两种类型都必须是多态的,而MyWriter 不是。另外dynamic_cast很贵。
    【解决方案5】:

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

    【讨论】:

      猜你喜欢
      • 2011-02-25
      • 1970-01-01
      • 2022-12-11
      • 2013-09-30
      • 2016-04-27
      • 2012-02-17
      • 2016-06-14
      • 1970-01-01
      • 2010-09-22
      相关资源
      最近更新 更多