纯虚函数和二进制兼容性

时间:2013-02-14 12:37:14

标签: c++ inheritance binary-compatibility

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

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

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 个答案:

答案 0 :(得分:5)

简单的答案是:不。任何时候你改变班级 all 中的定义,可能会失去二进制兼容性。 添加非虚函数或静态成员通常是安全的 在实践中,虽然仍然是正式未定义的行为,但是 这就是它。其他任何东西都可能打破二进制 兼容性。

答案 1 :(得分:5)

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

在这种情况下需要考虑的其他事情是,您不仅要提出ABI突破性更改,而且要破坏在编译时很难检测到的API。如果这些不是虚函数,并且ABI兼容性不是问题,那么在您进行更改之后,就像:

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

会悄悄地调用你的新函数,但只有重新编译该代码,才能使调试变得非常困难。

答案 2 :(得分:3)

您正在尝试描述流行的" Make classes non-derivedable" 技术,以保留二进制兼容性,例如,在 Symbian C ++中 em> API(查找 NewL 工厂方法):

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

    • 在类声明结尾添加虚函数
    • 添加数据成员并更改类的大小。
  3. 此技术仅适用于 GCC 编译器,因为它可以在二进制级别保存虚函数的源顺序。

    <强>解释

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

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

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

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

    工具

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

答案 3 :(得分:2)

当我遇到类似的情况时,对我来说非常了不起,我发现,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编译,则会出现史诗般的失败
  3. 结束......永远不要依赖二进制兼容性。任何类的更改都必须导致重新编译所有代码,使用它。