【问题标题】:How to design a C++ API for binary compatible extensibility如何为二进制兼容的可扩展性设计 C++ API
【发布时间】:2010-12-18 23:54:14
【问题描述】:

我正在为 C++ 库设计一个 API,该库将分布在 dll / 共享对象中。该库包含具有虚函数的多态类。我担心如果我在 DLL API 上公开这些虚函数,我就不可能用更多虚函数扩展相同的类,而不会破坏与为之前版本的库构建的应用程序的二进制兼容性。

一种选择是使用PImpl 习惯用法来隐藏所有具有虚函数的类,但这似乎也有其局限性:这样应用程序就失去了继承库的类并覆盖虚方法的可能性.

您将如何设计一个可以在应用程序中进行子类化的 API 类,同时又不会失去在新版本的 dll 中使用(非抽象)虚拟方法扩展 API 的可能性,同时保持向后二进制兼容?

更新:该库的目标平台是 windows/msvc 和 linux/gcc。

【问题讨论】:

  • 改用 C#。 ;-P

标签: c++ virtual binary-compatibility


【解决方案1】:

C++ 二进制兼容通常很困难,即使没有继承。以 GCC 为例。在过去的 10 年里,我不确定他们有多少破坏性的 ABI 更改。然后 MSVC 有一组不同的约定,因此无法使用 GCC 链接到它,反之亦然……如果将其与 C 世界进行比较,编译器互操作似乎更好一些。

如果您使用的是 Windows,则应该查看 COM。当您引入新功能时,您可以添加接口。然后调用者可以QueryInterface() 为新接口公开新功能,即使您最终更改了很多内容,您也可以将旧实现保留在那里,或者您可以为旧接口编写填充程序。

【讨论】:

  • “在过去的 10 年里,我不确定他们有多少破坏性的 ABI 更改”。让我告诉你有多少。 一个。当前的 ABI 已在标准文档中正式化和描述。
  • 我知道 2.95 和 3.0 之间有一个重大突破(这对 BeOS 和 Haiku 来说是一个严重的问题),但我似乎记得在 3.2 和 3.3 左右之间有另一个相当重大的突破(这导致在 Gentoo 上有点麻烦)。这是不正确的吗?
  • 哦,我以为 3.0 已经超过 10 年了。是的,两个。 2001 年 6 月发布 3.0。从那时起,他们致力于生产一个长期有效的 ABI 设计,并在 2002 年 8 月发布了 3.2 版本。七年前是最后一次。
  • 推荐COM解决二进制兼容性就像推荐氰化物治疗头痛一样。两者都会通过杀死你来解决问题:)
  • @Alek - 然而,Visual C++ 的每个版本都引入了一个不兼容的 C 运行时分支,其中一个 dll 中的 malloc 然后在另一个 dll 中释放会使程序崩溃,但 COM 对象继续工作。能够摆脱你可能认为是滥用的东西,看看它能给你带来什么好处,这很有帮助。
【解决方案2】:

几个月前,我写了一篇名为“在 GNU/Linux 系统上用 C++ 实现的共享库的二进制兼容性”[pdf] 的文章。虽然 Windows 系统上的概念相似,但我确信它们并不完全相同。但是阅读这篇文章后,您可以了解在 C++ 二进制级别发生的与兼容性有关的事情。

顺便说一句,GCC 应用程序二进制接口在标准文档草案“Itanium ABI”中进行了总结,因此您将有一个正式的基础来选择您选择的编码标准。

只是一个简单的例子:在 GCC 中,如果没有其他类继承它,您可以使用更多虚函数扩展一个类。阅读文章以获得更好的规则集。

但无论如何,规则有时过于复杂而难以理解。因此,您可能对验证两个给定版本的兼容性的工具感兴趣:abi-compliance-checker for Linux。

【讨论】:

  • 您发布的 PDF 文件的主机似乎已完成。请问可以转发一下吗?
  • @MichałGórny 它似乎又恢复了,但我已重新托管它here 以防万一。
【解决方案3】:

在 KDE 知识库上有一篇有趣的文章,描述了在编写库时针对二进制兼容性的注意事项:Policies/Binary Compatibility Issues With C++

【讨论】:

    【解决方案4】:

    我认为你误解了子类化的问题。

    这是你的 Pimpl:

    // .h
    class Derived
    {
    public:
      virtual void test1();
      virtual void test2();
    private;
      Impl* m_impl;
    };
    
    // .cpp
    struct Impl: public Base
    {
      virtual void test1(); // override Base::test1()
      virtual void test2(); // override Base::test2()
    
      // data members
    };
    
    void Derived::test1() { m_impl->test1(); }
    void Derived::test2() { m_impl->test2(); }
    

    看到了吗?重写Base 的虚方法没问题,您只需要确保在Derived 中重新声明它们virtual,以便那些从Derived 派生的人知道他们也可以重写它们(仅当您愿意时,由方式是为那些缺少它的人提供final 的好方法),您仍然可以在Impl 中为自己重新定义它,甚至可以调用Base 版本。

    那里Pimpl没有问题。

    另一方面,你失去了多态性,这可能很麻烦。由您决定是否需要多态性或只是组合。

    【讨论】:

    • Pimpl 的包装类应该有非虚方法,因为在这种情况下,它正好用于隐藏库类的虚方法。如果虚拟方法出现在库接口上,则无法在新版本中使用更多虚拟方法扩展库接口,同时保持二进制兼容。但是如果发布的接口是非虚拟的,那么客户端如何继承它呢?因此这篇文章。
    • 好的,那我明白你的意思了。但此时这并不是 Pimpl 的真正问题。更多关于在接口中使用virtual方法的问题。
    • “你只需要确保在 Derived 中重新声明它们是虚拟的,以便那些从 Derived 派生的人也可以重写它们”。不,被覆盖的虚方法也是隐含的虚方法。
    • @Frank:对于他们是编译器,对于读者来说,只有当它们被标记为这样时才很明显(因为没有人想挖掘包含)。我将进行编辑以使其更清晰。
    • 我阅读了引用的评论,因为您认为它对编译器也有影响。
    【解决方案5】:

    如果您在头文件中公开 PImpl 类,那么您可以从它继承。您仍然可以保持向后可移植性,因为外部类包含指向 PImpl 对象的指针。当然,如果库的客户端代码不是很聪明,它可能会滥用这个暴露的 PImpl 对象,并破坏二进制的向后兼容性。您可以在 PImpl 的头文件中添加一些注释来警告用户。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-10-11
      • 2011-08-03
      • 1970-01-01
      • 2011-08-09
      • 2020-05-12
      • 1970-01-01
      相关资源
      最近更新 更多