C++库二进制兼容Binary Compatible教程
FROM:http://www.cuteqt.com/?p=801C++库二进制兼容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改来改去,不影响字节长度。
要修改函数参数,只能增加一个新函数。这时候你一定要标上,等出大版本不要二进制兼容时,把这俩函数合一块:
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::DirectConnection。
如果子类要重新实现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来实现,也许还更加简单呢。
学习一下,谢谢分享~~:good :good 这d-pointer真很精致。内容不少,要好好学习了。
有个问题,原文说d-pointer里的数据都是private的,那么能不能用protected/public的d-pointer来实现需要protected/public访问的数据呢?
[ 本帖最后由 GPS 于 30-12-2009 21:52 编辑 ] 再翻出来赞一下。
刚刚碰到了二进制兼容的问题,赶紧将这篇翻出来又学一下。 原帖由 GPS 于 30-8-2010 13:48 发表 http://www.freeoz.org/ibbs/images/common/back.gif
再翻出来赞一下。
刚刚碰到了二进制兼容的问题,赶紧将这篇翻出来又学一下。
d pointer的设计从一个侧面显示了QT是C++库领域里很标准和和规范化的一个范例。 这也驳斥了C++无法保证ABI完整性的说法,只要严肃地对待C++的设计,C++在二进制兼容型上可以比C做得更好。 对的对的。
看来到了要严肃考虑 “严肃地对待C++的设计“ 的时候了。否则光重复代码或者从头来过的感觉时有发生。
我打算,私有的就用d-ptr, 共有的就用hash.
有个问题,按照LZ的文章,
如果用static global hash(或者一般的,其他global staticobject), 在类库里应该用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.
包括这种情况吗?
回复 #6 GPS 的帖子
static class member和global static 没什么区别。The code is not reentrant,是指,这个类的成员函数访问这个static member,和访问全局变量是一样的,当然不能保证reentrant了。
只有完全只访问类实例成员变量的类才是reentrant的。 再问一下,对于公有或者保护的成员,用成员hash表存,即每个公共成员是表里的一个记录,是不是也可以解决增加公共/保护成员的兼容问题。而且,可以用同一个accessor, QVariant access(const QString &key)? 另外发现个翻译错误。
以下修改方法是安全的:
* 增加私有成员。
应该是增加静态数据成员。 原帖由 GPS 于 30-8-2010 15:50 发表 http://www.freeoz.org/ibbs/images/common/back.gif
再问一下,对于公有或者保护的成员,用成员hash表存,即每个公共成员是表里的一个记录,是不是也可以解决增加公共/保护成员的兼容问题。而且,可以用同一个accessor, QVariant access(const QString &key)? 这个当然没问题了, C++不管你是用什么实现的,这个仅仅是一个方法而已。 再问个implicit sharing的问题。
如果用qshareddata和 qshareddatapointer来实现implicit sharing时(如document里的employee/employeedata class),怎样subclass呢? 难到对每个employee的subclass都要做一个相应employeedata的subclass?
不太懂。似乎不应该subclass. 原帖由 GPS 于 6-9-2010 10:48 发表 http://www.freeoz.org/ibbs/images/common/back.gif
再问个implicit sharing的问题。
如果用qshareddata和 qshareddatapointer来实现implicit sharing时(如document里的employee/employeedata class),怎样subclass呢? 难到对每个employee的subclass都要做一个相应 ...
可以subclassclass EmployeeData : public QSharedData{ //...};
class MyEmployeeData : public EmployeeData{ //... };
QSharedDataPointer<EmployeeData> d1;
QSharedDataPointer<MyEmployeeData> d2;
//------
d1 = new EmployeeData() ; //ok
d1 = new MyEmployeeData(); //ok, but only EmployeeData can be accessed
d2 = new MyEmployeeData(); //ok
d2 = new EmployeeData(); //error 问题是这样的.
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, whileonly _my_d_ptr is required.
Any idea?
回复 #13 GPS 的帖子
http://blog.csdn.net/oowgsoo/archive/2007/03/14/1529284.aspx 很复杂阿,要慢慢看。请问是你写的吗?很厉害。初步的印象,它直接使用指针来实现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 或者,
由于qt里的数据类,比如qstring, qbytearray都是implicit sharing, 是不是可以直接用
class employ 和 class myemploy来包装数据,而不用employddata 和myemploydata?
这样就可以达到implicit sharing的效果了? 原帖由 GPS 于 6-9-2010 14:08 发表 http://www.freeoz.org/ibbs/images/common/back.gif
或者,
由于qt里的数据类,比如qstring, qbytearray都是implicit sharing, 是不是可以直接用
class employ 和 class myemploy来包装数据,而不用employddata 和myemploydata?
这样就可以达到implicit sharing的效 ...
你如果只有一个QString成员,这么用当然没关系,如果类数据成员比较复杂就用d_ptr,如果逻辑更复杂,那就用Private class. 原帖由 GPS 于 6-9-2010 14:05 发表 http://www.freeoz.org/ibbs/images/common/back.gif
很复杂阿,要慢慢看。请问是你写的吗?很厉害。
初步的印象,它直接使用指针来实现d_ptr, 子类可以将子数据类指针付给父数据类指针。这里应该要自己实现implicit sharing的指针counting, 及跨线程使用。
但是我想用 ...
最完整地符合Qt风格的写法是这样://private classes
class EmployeePrivate : public QShareData
{
void foo();
};
class MyEmployeePrivate : public EmployeePrivate
{
void bar();
};
class Employee : public QObject
{
Q_OBJECT
public:
void foo()
{
d_ptr->foo();
}
private:
QSharedDataPointer<EmployeePrivate> d_ptr;
};
class MyEmployee : public Employee
{
Q_OBJECT
public:
void bar()
{
Q_D(const MyEmployee);
d->bar();
}
private:
Q_DECLARE_PRIVATE(MyEmployee)
}; 大概明白了,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。
页:
[1]