8

现在,我知道向非叶类添加新的虚函数通常是不好的,因为它破坏了任何尚未重新编译的派生类的二进制兼容性。但是,我的情况略有不同:

我有一个接口类和实现类编译成一个共享库,例如:

class Interface {
    public:
        static Interface* giveMeImplPtr();
        ...
        virtual void Foo( uint16_t arg ) = 0;
        ...
}

class Impl {
    public:
        ...
        void Foo( uint16_t arg );
        ....
}

我的主应用程序使用这个共享库,基本上可以写成:

Interface* foo = Implementation::giveMeImplPtr();
foo->Foo( 0xff );

换句话说,应用程序没有任何派生自 的类Interface,它只是使用它。

现在,假设我想用 超载Foo( uint16_t arg )Foo( uint32_t arg )我可以安全地做:

 class Interface {
    public:
        static Interface* giveMeImplPtr();
        ...
        virtual void Foo( uint16_t arg ) = 0;
        virtual void Foo( uint32_t arg ) = 0;
        ...
}

并重新编译我的共享库而无需重新编译应用程序?

如果是这样,我需要注意任何不寻常的警告吗?如果没有,除了获取命中和升级库之外,我是否还有其他选择,从而破坏了向后兼容性?

4

4 回答 4

8

简单回答是不。任何时候更改类定义,可能失去二进制兼容性。添加非虚拟函数或静态成员在实践中通常是安全的,尽管仍然是形式上未定义的行为,但仅此而已。其他任何事情都可能会破坏二进制兼容性。

于 2013-02-14T12:41:41.307 回答
7

ABI 基本上取决于对象的大小和形状,包括 vtable。添加虚函数肯定会改变vtable,如何改变取决于编译器。

在这种情况下要考虑的其他事情是,您不仅提出了 ABI 重大更改,而且还提出了在编译时很难检测到的 API 中断。如果这些不是虚拟功能并且 ABI 兼容性不是问题,那么在您进行更改后,类似于:

void f(Interface * i) {
  i->Foo(1)
}

将悄悄地最终调用您的新函数,但前提是重新编译该代码,这会使调试变得非常困难。

于 2013-02-14T14:09:44.547 回答
6

您正在尝试描述流行的“使类不可派生”技术以保持二进制兼容性,例如,在Symbian C++ API 中使用(查找NewL工厂方法):

  1. 提供工厂功能;
  2. 将 C++ 构造函数声明为私有(并且非导出非内联,并且该类不应有友元类或函数),这使得该类不可派生,然后您可以:

    • 在类声明的末尾添加虚函数,
    • 添加数据成员并更改类的大小。

该技术仅适用于GCC编译器,因为它将虚函数的源代码顺序保存在二进制级别。

解释

虚函数是由对象的v-table中的偏移量调用的,而不是由损坏的名称调用的。如果您只能通过调用静态工厂方法来获取对象指针并保留所有虚函数的偏移量(通过保存源顺序,在末尾添加新方法),那么这将是向后二进制兼容的。

如果您的类具有公共构造函数(内联或非内联),则兼容性将被破坏:

  • inline:应用程序将复制一个旧的 v-table 和该类的旧内存布局,这将与新库中使用的不同;如果您调用任何导出的方法或将对象作为参数传递给此类方法,则可能会导致分段错误的内存损坏;

  • non-inline:情况更好,因为您可以通过在叶类声明的末尾添加新的虚拟方法来更改 v-table,因为如果您愿意,链接器将在客户端重新定位派生类的 v-table 布局加载新的库版本;但是您仍然无法更改类的大小(即添加新字段),因为大小在编译时是硬编码的,并且调用新版本的构造函数可能会破坏客户端堆栈或堆上相邻对象的内存。

工具

尝试使用abi-compliance-checker工具检查 Linux 上类库版本的向后二进制兼容性。

于 2013-02-15T09:28:38.813 回答
3

当我处于类似情况时,我发现 MSVC颠倒了重载函数的顺序,这让我感到非常惊讶。根据您的示例,MSVC 将像这样构造 v_table (二进制):

virtual void Foo( uint32_t arg ) = 0;
virtual void Foo( uint16_t arg ) = 0;

如果我们扩大一点你的例子,像这样:

class Interface {
    virtual void first() = 0;
    virtual void Foo( uint16_t arg ) = 0;
    virtual void Foo( uint32_t arg ) = 0;
    virtual void Foo( std::string arg ) = 0;
    virtual void final() = 0;
}

MSVC 将构造以下 v_table:

    virtual void first() = 0;
    virtual void Foo( std::string arg ) = 0;
    virtual void Foo( uint32_t arg ) = 0;
    virtual void Foo( uint16_t arg ) = 0;
    virtual void final() = 0;

Borland builder 和 GCC 不改变顺序,但是

  1. 他们在我测试的那个版本中没有这个
  2. 如果您的库由 GCC 编译(例如),而应用程序将由 MSVC 编译,那将是一个史诗般的失败

结束...永远不要依赖二进制兼容性。类的任何更改都必须使用它重新编译所有代码。

于 2013-02-14T12:47:36.807 回答