找回密码
 FreeOZ用户注册
查看: 3694|回复: 18
打印 上一主题 下一主题

[论坛技术] C++库二进制兼容Binary Compatible教程

[复制链接]
跳转到指定楼层
1#
发表于 29-12-2009 10:05:58 | 只看该作者 回帖奖励 |倒序浏览 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有帐号?FreeOZ用户注册

x
FROM:http://www.cuteqt.com/?p=801
C++库二进制兼容Binary Compatible教程
本文是从KDE的一个扫盲文翻译而来。说翻译其实也不是翻译,照着意思写而已,与原文并不严格对照。
我翻译了俩小时,大家仔细看看啊~
原文: http://techbase.kde.org/index.ph ... 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改来改去,不影响字节长度。
要修改函数参数,只能增加一个新函数。这时候你一定要标上,等出大版本不要二进制兼容时,把这俩函数合一块:
void fun( int a );
void fun( int a, int b ); //等不用二进制兼容的,这俩合成一个 void fun(int a, int b=0);
为了保持一个类的扩展空间,应该遵守下列规则:
* 使用d-pointer指针指向私有类
* 即使没什么事儿可做,也要弄一个像回事似的非内联的虚析构函数。
* 对于显示部件,甭管有没有事可做,也要把所有的event函数都写了,占个位子先。
* 所有的构造函数都不要内联。
* 写拷贝初始化函数和赋值函数的时候,尽量不要内联。当然如果不能进行值拷贝的时候就没办法了,比如QObject子类都不行。
类库开发守则:
开发类库的人最头疼的就是无法给类增加数据成员,因为这样会破坏类的长度结构,甚至连累所有的子类。
一个解决办法就是利用位标志。比如你原来设计了一个类,里面有这么几个enum或者bool类型:
uint m1 : 1;
uint m2 : 3;
uint m3 : 1;
你要是把它改成这样也不会破坏二进制兼容:
uint m1 : 1;
uint m2 : 3;
uint m3 : 1;
uint m4 : 2; // new member
究其原因,是本来已经占用了足够的位数,增加一个位标志并没有让数据字节长度增加。注意尽量不要用最后一位,有的编译器会出问题的。
使用d-pointer
使用位标志和占位变量只是旁门左道。d-pointer是Qt开发者发明的一个保护二进制兼容的办法,也是Qt如此成功的原因之一。
假如你要声明一个类Foo的话,先声明一个它的私有类,用向前引用的方法:
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的实例:
d = new FooPrivate;
当然别忘记在析构函数里删掉它:
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.
* 很不幸的是大部分编译器都是闹残,在创建动态链接库的时候都不会自动创建静态对象,所以你要用Q_GLOBAL_STATIC宏来声明这个哈西表才行:
   1. //为了二进制兼容: 增加一个真正的d-pointer
   2. Q_GLOBAL_STATIC(QHash, 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)就可以了:
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.
  13. A::A()
  14. {
  15.     connect(this, SIGNAL( bar(int*)), this, SLOT( barslot(int*)));
  16. }
  17.
  18. void A::foo()
  19. {
  20.     int ret;
  21.     emit bar( &ret );
  22. }
  23.
  24. void A::barslot( int* ret )
  25. {
  26.     *ret = 10;
  27. }
函数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.
   9. B::B()
  10. {
  11.     disconnect(this, SIGNAL(bar(int*)), this, SLOT(barslot(int*)));
  12.     connect(this, SIGNAL(bar(int*)), this, SLOT(barslot(int*)));
  13. }
  14.
  15. void B::barslot( int* ret )
  16. {
  17.     *ret = 20;
  18. }
这 样B::barslot()就跟A::bar()的虚实现一样了。 barslot()必须声明为slot,而且在构造函数里,必须先disconnect然后再重新connect,这样才能偷梁换柱。当然,你也可以用 virtual slot来实现,也许还更加简单呢。

评分

参与人数 1威望 +50 收起 理由
trisun + 50 谢谢分享!

查看全部评分

回复  

使用道具 举报

2#
发表于 29-12-2009 12:42:19 | 只看该作者
学习一下,谢谢分享~~
回复  

使用道具 举报

3#
发表于 30-12-2009 21:48:35 | 只看该作者
这d-pointer真很精致。内容不少,要好好学习了。
有个问题,原文说d-pointer里的数据都是private的,那么能不能用protected/public的d-pointer来实现需要protected/public访问的数据呢?

[ 本帖最后由 GPS 于 30-12-2009 21:52 编辑 ]
回复  

使用道具 举报

4#
发表于 30-8-2010 14:48:51 | 只看该作者
再翻出来赞一下。
刚刚碰到了二进制兼容的问题,赶紧将这篇翻出来又学一下。
回复  

使用道具 举报

5#
 楼主| 发表于 30-8-2010 14:58:41 | 只看该作者

d pointer的设计从一个侧面显示了QT是C++库领域里很标准和和规范化的一个范例。 这也驳斥了C++无法保证ABI完整性的说法,只要严肃地对待C++的设计,C++在二进制兼容型上可以比C做得更好。
回复  

使用道具 举报

6#
发表于 30-8-2010 15:41:09 | 只看该作者
对的对的。
看来到了要严肃考虑 “严肃地对待C++的设计“ 的时候了。否则光重复代码或者从头来过的感觉时有发生。
我打算,私有的就用d-ptr, 共有的就用hash.
有个问题,按照LZ的文章,
如果用static global hash(或者一般的,其他global static  object), 在类库里应该用Q_GLOBAL_STATIC来确保这个static global object 被初始化。
那么,如果我将这个object定义成class的static member,再在全局初始化, 比如
class A
{
static QHash _hash;
}

QHash A::_hash;

这样可以吗?
下面的文章里
http://translated.by/you/qt-coding-conventions/original/?page=1
提到
Note: Static objects in a scope are no problem, the constructor will be run the first time the scope is entered. The code is not reentrant, though.
包括这种情况吗?
回复  

使用道具 举报

7#
 楼主| 发表于 30-8-2010 15:52:58 | 只看该作者

回复 #6 GPS 的帖子

static class member和global static 没什么区别。
The code is not reentrant,是指,这个类的成员函数访问这个static member,和访问全局变量是一样的,当然不能保证reentrant了。

只有完全只访问类实例成员变量的类才是reentrant的。
回复  

使用道具 举报

8#
发表于 30-8-2010 16:50:02 | 只看该作者
再问一下,对于公有或者保护的成员,用成员hash表存,即每个公共成员是表里的一个记录,是不是也可以解决增加公共/保护成员的兼容问题。而且,可以用同一个accessor, QVariant access(const QString &key)?
回复  

使用道具 举报

9#
发表于 30-8-2010 17:45:53 | 只看该作者
另外发现个翻译错误。
以下修改方法是安全的:
* 增加私有成员。

应该是增加静态数据成员。

评分

参与人数 1威望 +50 收起 理由
coredump + 50 你太有才了!

查看全部评分

回复  

使用道具 举报

10#
 楼主| 发表于 30-8-2010 17:48:30 | 只看该作者
原帖由 GPS 于 30-8-2010 15:50 发表
再问一下,对于公有或者保护的成员,用成员hash表存,即每个公共成员是表里的一个记录,是不是也可以解决增加公共/保护成员的兼容问题。而且,可以用同一个accessor, QVariant access(const QString &key)?
这个当然没问题了, C++不管你是用什么实现的,这个仅仅是一个方法而已。
回复  

使用道具 举报

11#
发表于 6-9-2010 11:48:39 | 只看该作者
再问个implicit sharing的问题。
如果用qshareddata  和 qshareddatapointer来实现implicit sharing时(如document里的employee/employeedata class),怎样subclass呢? 难到对每个employee的subclass都要做一个相应employeedata的subclass?
不太懂。似乎不应该subclass.
回复  

使用道具 举报

12#
 楼主| 发表于 6-9-2010 12:03:12 | 只看该作者
原帖由 GPS 于 6-9-2010 10:48 发表
再问个implicit sharing的问题。
如果用qshareddata  和 qshareddatapointer来实现implicit sharing时(如document里的employee/employeedata class),怎样subclass呢? 难到对每个employee的subclass都要做一个相应 ...

可以subclass
  1. class EmployeeData : public QSharedData{    //...};
  2. class MyEmployeeData : public EmployeeData{        //...    };
  3. QSharedDataPointer<EmployeeData> d1;
  4. QSharedDataPointer<MyEmployeeData> d2;
  5. //------
  6. d1 = new EmployeeData() ; //ok
  7. d1 = new MyEmployeeData(); //ok, but only EmployeeData can be accessed
  8. d2 = new MyEmployeeData(); //ok
  9. d2 = new EmployeeData(); //error
复制代码
回复  

使用道具 举报

13#
发表于 6-9-2010 14:21:45 | 只看该作者
问题是这样的.

class EmployData : QSharedData
{
public:
  QByteArray _data;
};

class Employ
{
private:
  QSharedDataPointer< EmployData> _d_ptr;
};

Now, subclass Employ --
class MyEmployData : public EmployData
{
public:
QByteArray _myData;
};

class MyEmploy: public Employ
{
private:
QSharedDataPointer<MyEmployData> _my_d_ptr;
};
Now, MyEmploy has both _d_ptr and _my_d_ptr, while  only _my_d_ptr is required.
Any idea?
回复  

使用道具 举报

14#
 楼主| 发表于 6-9-2010 14:38:28 | 只看该作者
回复  

使用道具 举报

15#
发表于 6-9-2010 15:05:17 | 只看该作者
很复杂阿,要慢慢看。请问是你写的吗?很厉害。
初步的印象,它直接使用指针来实现d_ptr, 子类可以将子数据类指针付给父数据类指针。这里应该要自己实现implicit sharing的指针counting, 及跨线程使用。
但是我想用QSharedData 和 Qshareddatapointer 来实现implicit sharing,指针counting等都不用考虑。但是不能够将 Qshareddatapointer<myemploydata> 赋给qsharedpointer<employdata>, 不知道怎样解决。
这是qt doc给的例子。
http://doc.qt.nokia.com/4.6/qshareddatapointer.html

评分

参与人数 1威望 +30 收起 理由
coredump + 30 不是啊,是Qt的标准Style

查看全部评分

回复  

使用道具 举报

16#
发表于 6-9-2010 15:08:54 | 只看该作者
或者,
由于qt里的数据类,比如qstring, qbytearray都是implicit sharing, 是不是可以直接用
class employ 和 class myemploy来包装数据,而不用employddata 和myemploydata?
这样就可以达到implicit sharing的效果了?
回复  

使用道具 举报

17#
 楼主| 发表于 6-9-2010 16:30:04 | 只看该作者
原帖由 GPS 于 6-9-2010 14:08 发表
或者,
由于qt里的数据类,比如qstring, qbytearray都是implicit sharing, 是不是可以直接用
class employ 和 class myemploy来包装数据,而不用employddata 和myemploydata?
这样就可以达到implicit sharing的效 ...

你如果只有一个QString成员,这么用当然没关系,如果类数据成员比较复杂就用d_ptr,如果逻辑更复杂,那就用Private class.
回复  

使用道具 举报

18#
 楼主| 发表于 6-9-2010 17:20:30 | 只看该作者
原帖由 GPS 于 6-9-2010 14:05 发表
很复杂阿,要慢慢看。请问是你写的吗?很厉害。
初步的印象,它直接使用指针来实现d_ptr, 子类可以将子数据类指针付给父数据类指针。这里应该要自己实现implicit sharing的指针counting, 及跨线程使用。
但是我想用 ...

最完整地符合Qt风格的写法是这样:
  1. //private classes
  2. class EmployeePrivate : public QShareData
  3. {
  4.     void foo();
  5. };
  6. class MyEmployeePrivate : public EmployeePrivate
  7. {
  8.     void bar();
  9. };

  10. class Employee : public QObject
  11. {
  12.      Q_OBJECT
  13. public:
  14.      void foo()
  15.      {
  16.          d_ptr->foo();
  17.      }
  18. private:
  19.     QSharedDataPointer<EmployeePrivate> d_ptr;
  20. };

  21. class MyEmployee : public Employee
  22. {
  23.      Q_OBJECT
  24. public:
  25.      void bar()
  26.      {
  27.          Q_D(const MyEmployee);
  28.          d->bar();
  29.      }
  30. private:
  31.     Q_DECLARE_PRIVATE(MyEmployee)
  32. };
复制代码
回复  

使用道具 举报

19#
发表于 6-9-2010 19:12:05 | 只看该作者
大概明白了,Q_DECLARE_PRIVATE 里 用 reinterpret_cast来cast 子类的d_ptr.
查了一下,QObjectData和qshareddata并没有继承关系,就是说qt 内部的类使用 qobject/qobjectdata来实现d_ptr和implicit sharing, 而提供给用户另一套qshareddata/qshareddatapointer, 不过看起来,对于d_ptr部分,处理类似,都使用了Q_D, Q_DECLARE_PRIVATE这些宏。
这个理解对不对阿?
多谢coredump。
回复  

使用道具 举报

您需要登录后才可以回帖 登录 | FreeOZ用户注册

本版积分规则

小黑屋|手机版|Archiver|FreeOZ论坛

GMT+11, 1-11-2024 22:31 , Processed in 0.060076 second(s), 35 queries , Gzip On, Redis On.

Powered by Discuz! X3.2

© 2001-2013 Comsenz Inc.

快速回复 返回顶部 返回列表