提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
前言
对于C-Like等编译型语言,没有虚拟机的协助,随着项目维护,必然会产生很多兼容性问题。此文作为个人对其中的二进制兼容性进行一下总结。
一、什么是二进制兼容?
对于一个库,如果使用旧版本的主程序,无需重新编译就可以连接新版本的库,并正常运行,则这个库是二进制兼容的。
如果主程序不需要修改,但需要重新编译才可以使用新版本的库,则认为这个库是源代码兼容的。
二进制兼容可以避免很多问题。可以让在特定平台上发布软件更容易。如果各个发布版之间不保证二进制兼容,人们就不得不提供静态链接的二进制文件。静态二进制文件有如下弊端:
1.浪费资源(尤其是内存);
2.主程序无法从库的bug修复和扩展中获益;
注: 这里提到的限制可能不适用与特定的编译器。目的是列出最严格的条件集合。这些条件集合在编写跨平台的C++代码(意味着需要使用不同的编译器编译)时,对保持二进制兼容有益。
二、保持二进制兼容的条件
1.可以做的事情
- 给类添加枚举类型
- 向已存在的枚举内添加新的枚举值
- 例外:如果此操作导致编译器需要为枚举选择更大的基础类型,则会破坏二进制兼容。不幸地是,编译器会自动为枚举类型选择基础类型。所以,从API设计的角度考虑,可以直接指定最大的枚举值(=255,等)来创建一个枚举值区间来保证编译器选择合适的基础类型。
- 重新实现在最底层的基类中定义的虚函数(在第一个非虚基类定义的虚函数)。一个前提是,连接到旧版本库的程序调用基类的实现而不是子类的实现时,是安全的的。(此修改很棘手,并且很危险。在修改前请三思)
- 例外:如果重写的函数有协变返回类型,派生类型总是和上层类型有相同的指针地址,此修改才是二进制兼容的。如果不确定,不要重写有协变返回类型的函数
- 修改inline函数或者把inline函数修改为non-inline函数,前提是使用旧版本库的主程序调用的是旧的实现。(此修改很棘手,并且很危险。在修改前请三思)
- 移除私有的非虚函数(如果此函数从未被inline函数调用)
- 移除私有的静态成员(如果此成员从未被inline函数调用)
- 添加静态数据成员
- 修改方法的默认参数。如果主程序需要使用修改后的新默认值,则需要重新编译
- 添加新的类
- 导出原来没有导出的类
- 添加或删除friend声明
- 重命名已有的成员类型
- 扩展已存在的位域(扩展后不能超过基础类型的范围)
- 添加Q_OBJECT宏到类(此类需要已从QObject继承)
- 添加Q_PROPERTY,Q_ENUMS或Q_FLAGS宏(此操作需只修改moc生成元对象,而不是class本身)
- 添加非虚函数,包括signal、slot和构造函数,前提是它们没有重载非重载函数
2.不可以做的事情
- 对已存在的类
- 把导出的类修改为非导出,或删除
- 任何更改类层次结构的操作(添加,删除,重排基类顺序)
- 移除final关键字
- 对模板类
- 任何更改模板参数的操作(添加,删除,重新排序)
- 对已存在的函数
- 修改为非导出
- 删除
- 删除已声明函数的实现。符号来自函数的实现,所以实现才是有效的。
- 修改为inline(包括把函数体移到类定义文件中)
- 添加overload关键字(二进制兼容,但源码不兼容:会使&func有歧义)。给已经重载的函数添加overload是可以的
- 修改函数签名。包括:
- 修改参数列表里的参数类型,包括const、volatile修饰
- 修改函数的const、volatile修饰符
- 修改函数或数据成员的访问权限。对某些编译器,访问权限是签名信息的一部分。如果你需要把一个函数从private修改为protected或public,可以添加一个新函数来调用原来的private函数
- 使用参数来扩展函数,即使这个函数有默认值
- 任何修改返回值类型的操作
- 例外:使用extern "C"声明的非成员函数可以修改参数类型(要十分小心)
- 对虚函数:
- 添加虚函数到类中,此类原来没有虚函数或虚基类
- 添加虚函数到有子类的类中。
- 如果要在Windows下保持二进制兼容,则任何添加新虚函数的都是不被允许的,即使是leaf类。因为这么做可能引发对已存在函数的重新排序,从而破坏二进制兼容。
- 更改类声明中虚函数的顺序
- 重写当前类的非虚基类中的虚函数
- 重写已存在的虚函数,如果此函数有协变返回类型,且协变类型在不同层的继承关系中指针地址不同(通常出现在,继承层次,多继承或虚继承中)
- 对静态非私有成员或非静态非成员公共数据:
- 删除或由导出修改为不再导出
- 改变类型
- 改变const / volatile修饰符
- 对非静态成员
- 添加新数据成员
- 改变非静态数据成员在类中的顺序
- 改变成员类型。修改类型后成员size不变并且没有被任何inline函数使用的除外。
- 删除非静态数据成员
- 在公共API中,返回(或作为参数)一个迭代器给一个Qt容器。如果库使用了QT_STRICT_ITERATORS编译,但是使用API的却没有,则这样不是二进制兼容的,反之亦然。替代方案是,返回引用或拷贝给容器。
补充说明
如果需要添加或修改已存在的函数的参数列表,可以添加一个带有新参数的函数来作为替代方案。这种情况下,你需要添加一个简单的说明,来表明这两个函数应该使用默认参数的方式在以后的更新中进行合并。
void function(int a);
void function(int a, int b); // BCI: merge with int b = 0
应该做的
为了保持类未来的扩展性,应该遵循的规则:
1.添加 d指针
2.添加非内联的虚析构函数,即使函数体为空
3.重新实现QObject子类的event,即使函数体中只是调用基类的实现。(处理5中的例外情况)
4.所有的构造函数都不能inline
5.实现非内联的拷贝构造函数和复制运算符,除非这个类不能进行值拷贝(例如继承自QObject)
总结
以上是对于C++及Qt二进制兼容的一些总结。后续会进行一些代码验证。