C++库二进制兼容Binary Compatible教程

本文是从KDE的一个扫盲文翻译而来。说翻译其实也不是翻译,照着意思写而已,与原文并不严格对照。
我翻译了俩小时,大家仔细看看啊~

原文: http://techbase.kde.org/index.php?title=Policies/Binary_Compatibility_Issues_With_C%2B%2B

什么是二进制兼容

二进制兼容是针对动态链接库而言的。如果一个程序原来用旧版的库玩得很好,你偷偷给换成新版的库,他照样玩得很开心,甚至都不知道库换了,那你这个库就二进制兼容了。如果换了库就要重新编译一下才能继续玩,那叫源代码兼容(source compatible)。如果换了库就怎么也玩不转了,那叫不兼容。总地来说就是新的库文件保持和老的一样的二进制接口,让程序还得能直接找得着,用得了。

为啥要二进制兼容呢?当然为了省事儿。想象你发布软件的时候,如果不二进制兼容,就得所有文件重新发布一遍,多麻烦。当然也可以发布静态链接的软件,但那就更傻了,浪费时间资源不说,每次修改一个小bug就得重新下载所有文件。KDE就不傻,每一个大版本之内,比如4.2.x都二进制兼容,回头哪个文件有毛病下一个新版本补丁一下就好了,这也是没办法,让bug催的。

本文用的标准是GCC 3.4以上广泛使用的Itanium C++ ABI(程序二进制接口标准)。但不保证对所有编译器有效。

如何保证二进制兼容


要保证二进制兼容,修改源代码时一定要小心,有的事情能干,有的一定不能干。总的原则两条,第一,不改变编译器用于命名函数的关键结构,第二,不改变数据堆栈的长度和结构。

以下修改方法是安全的:

    * 增加非虚函数,增加signal/slots,构造函数什么的。
    * 增加枚举enum或增加枚举中的项目。
    * 重新实现在父类里定义过的虚函数 (就是从这个类往上数的第一个非虚基类),理论上讲,程序还是找那个基类要这个虚函数的实现,而不是找你新写的函数要,所以是安全的。但是这可不怎么保准儿,尽量少用。(好多废话,结论是少用)
          o 有一个例外: C++有时候允许重写的虚函数改变返回类型,在这种情况下无法保证二进制兼容。
    * 修改内联函数,或者把内联函数改成非内联的。这也很危险,尽量少用。
    * 去掉一个私有非虚函数。如果在任何内联函数里用到了它,你就不能这么干了。
    * 去掉私有的静态成员。同样,如果内联函数引用了它,你也不能这么干。
    * 增加私有成员。
    * 修改函数参数的缺省值。(这个脑残:修改了缺省值肯定要重新编译,怎么可能二进制兼容)
    * 增加新类。
    * 对外开放一个新类。
    * 增减类的友元声明。
    * 修改保留成员的类型。
    * 把原来的成员位宽扩大缩小,但扩展后不得越过边界(char和bool不能过8位界,short不能过16位界,int不过32位界,以此类推)这个也接近闹残:原来没用到的那么几个位我扩来扩去当然没问题,可是这样实在是不让人放心。

以下修改方法是严格禁止的:

    * 对于已经存在的类:
          o 本来对外开放了,现在想收回来不开放
          o 换爹 (加爹,减爹,重新给爹排座次).
    * 对于类模板来说:
          o 修改任何模板参数(增减或改变座次)
    * 对于函数来说:
          o 不再对外开放
          o 彻底删掉
          o 改成内联的(把代码从类定义外头移到头文件的类定义里头也算改内联)。
          o 改变函数特征串:
                + 修改参数,包括增减参数或函数甚至是成员函数的const/volatile描述符。如果一定要这么干,增加一个新函数吧。
                + 把private改成protected或者public。如果一定要这么干,增加一个新函数吧。
                + 对于非成员函数,如果用extern "C"声明了,可以很小心地增减函数参数而不破坏二进制兼容。
    * 对于虚成员函数来说:
          o 给没虚函数或者虚基类的类增加虚函数
          o 修改有别的类继承的基类
          o 修改虚函数的前后顺序
          o 如果一个函数不是在往上数头一个非虚基类中声明的,覆盖它会造成二进制不兼容。
          o 如果虚函数被覆盖时改变了返回类型,不要修改它。
    * 对于非私有静态函数和非静态的非成员函数:
          o 改成不开放的或者删除
          o 修改类型或者const/violate
    * 对于非静态成员函数:
          o 增加新成员
          o 给非静态成员重新排序或者删除
          o 修改成员的类型, 有个例外就是修改符号:signed/unsigned改来改去,不影响字节长度。

要修改函数参数,只能增加一个新函数。这时候你一定要标上,等出大版本不要二进制兼容时,把这俩函数合一块:

  1. void fun( int a );
  2. void fun( int a, int b ); //等不用二进制兼容的,这俩合成一个 void fun(int a, int b=0);
复制代码

为了保持一个类的扩展空间,应该遵守下列规则:

    * 使用d-pointer指针指向私有类
    * 即使没什么事儿可做,也要弄一个像回事似的非内联的虚析构函数。
    * 对于显示部件,甭管有没有事可做,也要把所有的event函数都写了,占个位子先。
    * 所有的构造函数都不要内联。
    * 写拷贝初始化函数和赋值函数的时候,尽量不要内联。当然如果不能进行值拷贝的时候就没办法了,比如QObject子类都不行。

类库开发守则:

开发类库的人最头疼的就是无法给类增加数据成员,因为这样会破坏类的长度结构,甚至连累所有的子类。

一个解决办法就是利用位标志。比如你原来设计了一个类,里面有这么几个enum或者bool类型:

  1. uint m1 : 1;
  2. uint m2 : 3;
  3. uint m3 : 1;

你要是把它改成这样也不会破坏二进制兼容:

  1. uint m1 : 1;
  2. uint m2 : 3;
  3. uint m3 : 1;
  4. uint m4 : 2; // new member

究其原因,是本来已经占用了足够的位数,增加一个位标志并没有让数据字节长度增加。注意尽量不要用最后一位,有的编译器会出问题的。

使用d-pointer

使用位标志和占位变量只是旁门左道。d-pointer是Qt开发者发明的一个保护二进制兼容的办法,也是Qt如此成功的原因之一。

假如你要声明一个类Foo的话,先声明一个它的私有类,用向前引用的方法:

  1. class FooPrivate;

在类Foo里,声明一个指向FooPrivate的指针:

  1. private:
  2.     FooPrivate* d;

FooPrivate类本身在实现文件.cpp里定义,不需要头文件:

  1. class FooPrivate {
  2. public:
  3.     FooPrivate()
  4.         : m1(0), m2(0)
  5.     {}
  6.     int m1;
  7.     int m2;
  8.     QString s;
  9. };

在类Foo的构造函数里,创建一个FooPrivate的实例:

  1. d = new FooPrivate;

当然别忘记在析构函数里删掉它:

  1. delete d;

还有一个技巧,在大部分环境下,把d-pointer声明成const是比较明智的。这样可以避免意外修改和拷来拷去,避免内存泄露:

  1. private:
  2.     FooPrivate* const d;

这样,你可以修改d指向的内容,但是不能修改指针本身。

有时候,一些成员并不适合放在私有数据对象里。比如比较常用的对象,放在里面就很麻烦。内联函数也无法访问d-pointer指向的数据。另外,所有d-pointer里存储的对象都是私有的,要共有/保护访问,就要弄个get/set函数,跟Java那样:

  1. QString Foo::string() const
  2. {
  3.     return d->s;
  4. }
  5. void Foo::setString( const QString& s )
  6. {
  7.     d->s = s;
  8. }

常见的问题:

我的类没有d-pointer,我还想加新成员,这可怎么是好啊?

有空的位标志,预留变量没?要是都没有就麻烦了。不过麻烦不代表没有办法,如果你类继承自QObject,你可以把成员类挂到其中一个child上,然后想办法找这个child。还有更不要脸的办法,就是用一个哈西表保存你的对象和新成员的对应关系,要引用的时候上哈西表里找。比如说你可以用QHash或者QPtrDict。
对于忘记设计d-pointer的类,最标准的弥补做法是:
    * 设计一个私有类FooPrivate.
    * 创建一个静态的哈西表 static QHash<Foo *, FooPrivate>.
    * 很不幸的是大部分编译器都是闹残,在创建动态链接库的时候都不会自动创建静态对象,所以你要用Q_GLOBAL_STATIC宏来声明这个哈西表才行:

  1. //为了二进制兼容: 增加一个真正的d-pointer
  2. Q_GLOBAL_STATIC(QHash<Foo *,FooPrivate>, d_func);
  3. static FooPrivate* d( const Foo* foo )
  4. {
  5.     FooPrivate* ret = d_func()->value( foo, 0 );
  6.     if ( ! ret ) {
  7.         ret = new FooPrivate;
  8.         d_func()->insert( foo, ret );
  9.     }
  10.     return ret;
  11. }
  12. static void delete_d( const Foo* foo )
  13. {
  14.     FooPrivate* ret = d_func()->value( foo, 0 );
  15.     delete ret;
  16.     d_func()->remove( foo );
  17. }

这样你就可以在类里自由增减成员对象了,就好像你的类拥有了d-pointer一样,只要调用d(this)就可以了:

  1. d(this)->m1 = 5;

* 析构函数也要加入一句:

  1. delete_d(this);

* 记得加入二进制兼容(BCI)的标志,下次大版本发布的时候赶紧修改过来。
    * 下次设计类的时候,别再忘记加入d-pointer了。

如何覆盖已实现过的虚函数?

前文说过,如果爹类已经实现过虚函数,你覆盖是安全的:老的程序仍然会调用父类的实现。假如你有如下类函数:

  1. void C::foo()
  2. {
  3.     B::foo();
  4. }

B::foo()被直接调用。如果B机成了A,A中有foo()的实现,B中却没有foo()的实现,则C::foo()会直接调用A::foo()。如果你加入了一个新的B::foo()实现,只有在重新编译以后,C::foo()才会转为调用B::foo()。
一个善解人意的例子:

  1. B b;                // B 继承 A
  2. b.foo();

如果B的上一版本链接库根本没B::foo()这个函数,你调用foo()时一般不会访问虚函数表,而是直接调用A::foo()。

如果你怕用户重新编译时造成不兼容,也可以把A::foo() 改为一个新的保护函数 A::foo2(),然后用如下代码修补:

  1. void A::foo()
  2. {
  3.     if( B* b = dynamic_cast< B* >( this ))
  4.         b->B::foo(); // B:: 很重要
  5.     else
  6.         foo2();
  7. }
  8. void B::foo()
  9. {
  10.     // 新的函数功能
  11.     A::foo2(); // 有可能要调用父类的方法
  12. }

所有调用B类型的函数foo()都会被转到 B::foo().只有在明确指出调用A::foo()的时候才会调用A::foo()。

增加新类

拓展类功能的简单方法是在类上增加新功能的同时保留老功能。但是这样也限制了使用旧版链接库的类进行升级。对于那些小的要求高性能的类来说,要升级的时候,重新写一个类完全代替原来的才是更好的办法。

给非基类增加虚函数

对于那些没有其他类继承的类,可以增加一个相似的类,实现新的功能,然后修改应用程序使用这些新的功能。

  1. class A {
  2. public:
  3.     virtual void foo();
  4. };
  5. class B : public A { // 新增加的类
  6. public:
  7.     virtual void bar(); // 新增加的虚函数
  8. };
  9. void A::foo()
  10. {
  11.     // 这里要调用新的虚函数了
  12.     if( B* this2 = dynamic_cast< B* >( this ))
  13.         this2->bar();
  14. }

如果有其他类继承这个类,就不能这么干了。

如何使用signal代替虚函数

Qt的signal/slot有自己的虚函数表,因此,修改signal/slot不会影响二进制兼容。signal/slot也可以用来模拟虚函数:

  1. class A : public QObject {
  2. Q_OBJECT
  3. public:
  4.     A();
  5.     virtual void foo();
  6. signals:
  7.     void bar( int* ); // 增加的所谓虚函数,其实是个signal
  8. protected slots:
  9.     // implementation of the virtual function in A
  10.     void barslot( int* );
  11. };
  12. A::A()
  13. {
  14.     connect(this, SIGNAL( bar(int*)), this, SLOT( barslot(int*)));
  15. }
  16. void A::foo()
  17. {
  18.     int ret;
  19.     emit bar( &ret );
  20. }
  21. void A::barslot( int* ret )
  22. {
  23.     *ret = 10;
  24. }

函数bar()就像一个虚函数一样, barslot()实现了它的实际功能。一个限制就是signal只能返回void,你要传回值就只能用参数引用了。在Qt4中,要这么干必须把连接方式置为Qt:irectConnection。

如果子类要重新实现bar()就要增加自己的slot:

  1. class B : public A {
  2. Q_OBJECT
  3. public:
  4.     B();
  5. protected slots: //必须重新声明为slot:
  6.     void barslot( int* ); //重新实现barslot
  7. };
  8. B::B()
  9. {
  10.     disconnect(this, SIGNAL(bar(int*)), this, SLOT(barslot(int*)));
  11.     connect(this, SIGNAL(bar(int*)), this, SLOT(barslot(int*)));
  12. }
  13. void B::barslot( int* ret )
  14. {
  15.     *ret = 20;
  16. }

这样B::barslot()就跟A::bar()的虚实现一样了。 barslot()必须声明为slot,而且在构造函数里,必须先disconnect然后再重新connect,这样才能偷梁换柱。当然,你也可以用virtual slot来实现,也许还更加简单呢。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值