C++的策略/二进制兼容性问题

我们在coding-style中提到了C++二进制兼容性问题,这里我们也来学习下。


定义

库是二进制兼容的,如果动态链接到该库的旧版本的程序,无需重新编译,就可以与该库的新版本一起运行。

库是源代码兼容的,如果对于库的新版本,程序需要重新编译才能运行,但不需要任何进一步的修改。

二进制兼容性解决了很多麻烦。 它使为特定平台分发软件变得更加容易。 如果不能确保发行版之间的二进制兼容性,人们将不得不提供静态链接的二进制文件。 静态二进制文件很糟糕,因为它们

  • 浪费资源(尤其是内存)
  • 程序不能从库的bug修复或功能扩展中受益

在KDE项目中,对于核心库(kdelibs,kdepimlibs)的主要发行版的生命周期,我们提供二进制兼容性。

ABI注意事项

本文适用于KDE构建用的编译器使用的大多数C++ ABI。它主要基于Itanium C++ ABI草案,GCC C++编译器从3.4版本开始使用。 有关Microsoft Visual C++名称改编方案的信息主要来自于关于调用约定的文章(这是迄今为止有关MSVC ABI和名称改编的最完整信息)。

此处指定的某些约束可能不适用于给定的编译器。 此处的目标是列出最严格的一组条件,用于编写跨平台C++代码,这意味着会被多种不同的编译器编译。

发现新的二进制不兼容问题时,此页面会同步更新。

可做与不可做

你可以...

  • 添加新的非虚拟功能,包括信号和槽以及构造函数。

  • 在类中添加一个新的枚举。

  • 将新的枚举值追加到现有枚举。

    • 例题:如果这导致编译器为枚举选择了更大的基础类型,则这更改是二进制不兼容的。不幸的是,编译器有一些选择基础类型的余地,因此从API设计的角度来看,建议添加一个Max....枚举值,并显式设置一个较大的值(=255, =1<<15, 等),以创建一个保证可以适合所选基础类型的枚举值的范围。
  • 重新实现在类层次结构中原始基类定义的虚函数(该虚函数定义在第一个非虚基类,或该类的第一个非虚基类中,依此类推),如果程序链接到先前版本的库,且该库调用了基类中的实现而不是派生类中的实现,是安全的。这很棘手,可能很危险。三思而后行。或者,请参阅下面的解决方法。

    • 例外:如果重新实现的函数具有协变返回类型,则如果派生更多的类型始终与派生较小的类型具有相同的指针地址,则它仅是二进制兼容性的变动。如有疑问,请勿使用协变返回类型来进行覆写。
  • 更改内联函数或使内联函数变为非内联,如果程序链接到先前版本的库,且该库调用了旧的实现,是安全的。这很棘手,可能很危险。三思而后行。

  • 移除私有非虚函数,如果它们没有被任何内联函数调用过(并且从未使用过)。

  • 移除私有静态成员变量,如果它们没有被任何内联函数调用过(并且从未使用过)。

  • 添加新的静态数据成员变量。

  • 更改方法的默认参数。但是,它需要重新编译才能使用实际的新的默认参数值。

  • 添加新类。

  • 导出以前未导出的类。

  • 在类中添加或删除友元声明。

  • 重命名保留的成员类型。

  • 扩展保留的位字段,前提是这不会导致位字段超出其基础类型的边界(8 bits for char & bool, 16 bits for short, 32 bits for int, etc.)

  • 将Q_OBJECT宏添加到类中,如果该类已经从QObject继承。

  • 添加Q_PROPERTY,Q_ENUMS或Q_FLAGS宏,因为它仅修改由moc生成的元对象,而不修改类本身。

你不可以...

  • 对于现有类:

  • 对于模板类:
  • 对于任意类型的现有函数:
    • 取消导出。
    • 移除。
      • 移除已声明函数的实现。符号来自函数的实现,因此实际上就是函数。
    • 变为内联(这包括将成员函数的主体移至类定义,即使没有inline关键字也是如此)
    • 添加一个新的重载函数(BC,而不是SC:使&func变得模棱两可),将重载函数添加到已经被重载的函数中是可以的(对&func的任何使用都已经需要强制转换)。
    • 更改其签名。这包括:
      • 更改参数列表中参数的任意类型,包括更改现有参数的const/volatile限定符(代替,添加新方法)
      • 更改函数的const/volatile限定符
      • 更改某些函数或数据成员变量的访问权限,例如从私有公有。对于某些编译器,此信息可能是签名的一部分。如果需要使私有函数变为受保护的甚至是共有的,则必须添加一个新函数,来调用此私有函数。
      • 更改成员函数的CV限定符:应用于函数本身的const和/或volatile。
      • 使用其他参数扩展函数,即使此参数具有默认参数。请参阅以下有关如何避免此问题的建议。
      • 以任何方式更改返回类型
      • 例外:用extern "C"声明的非成员函数可以更改参数类型(请小心)。
  • 对于虚成员函数:
  • 对于静态非私有成员或非静态非成员公有数据:

    - 移除或取消导出

    - 更改其类型

    - 更改其CV限定符
  • 对于非静态成员变量:

    - 添加新的数据成员到现有的类中。

    - 更改类中非静态数据成员的顺序。

    - 更改成员的类型(变量名符号除外)

    - 从已有的类中删除已有的非静态数据成员。

如果需要添加扩展/修改现有函数的参数列表,则需要添加新函数,而不是新参数。在这种情况下,您可能想添加简短说明,在库的更高版本中,这两个函数应通过默认参数进行合并:

void functionname( int a );
void functionname( int a, int b ); //BCI: merge with int b = 0

你应该...

为了类在将来可扩展,您应该遵循以下规则:

  • 添加d指针。见下文。
  • 添加非内联虚析构函数,即使主体为空。
  • 在QObject派生的类中重新实现event,即使新函数的主体只是调用基类的实现。这是专门为避免,因添加已重新实现的虚函数而引起的问题,如下所述。
  • 使所有构造函数非内联。
  • 编写拷贝构造函数和赋值运算符的非内联实现,除非类无法进行值拷贝(例如,从QObject继承的类是不能的)

库程序员的技巧

编写库时最大的问题是,不能安全地添加数据成员,因为这会改变每个class类,struct结构,或者对象类型数组的大小和布局。

位标志

位标志是一种例外。 如果对枚举或布尔使用位标志,则至少在下一个字节减去1bit之前是安全的。具有下面成员的类

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

不会破坏二进制兼容性。 请四舍五入到最多7位(如果已经大于8,则为15位)。使用最后一位可能会在某些编译器上引起问题。

使用d指针

位标记和预定义的保留变量很好,但远远不够。这就是d指针技术发挥作用的地方。"d指针"的名称源于Trolltech's Arnt Gulbrandsen,他首先将该技术引入到Qt,使其成为最早的C++ GUI库之一,用于在更大的发行版之间保持二进制兼容性。看到它的每个人都迅速将该技术用作KDE库的通用编程模式。这是一个绝妙的技巧,能够在不破坏二进制兼容性的情况下将新的私有数据成员添加到类中。

备注:d指针模式在计算机科学历史上已经以不同的名称被多次描述过,例如pimpl,handle/body或cheshire cat。Google可以帮助您找到其中任何一种的在线论文,只需将C++添加到搜索词中即可。

在类Foo的定义中,定义一个前向声明

class FooPrivate;

和私有成员中的d指针:

private:
FooPrivate* d;

FooPrivate类本身完全定义在类实现文件(通常为*.cpp)中,例如:

class FooPrivate {
public:
FooPrivate()
: m1(0), m2(0)
{}
int m1;
int m2;
QString s;
};

您现在要做的就是,在构造函数或初始化函数中使用以下方法创建私有数据:

d = new FooPrivate;

并在析构函数中将其删除

delete d;

在大多数情况下,您将需要使d指针为const,以捕获意外修改或拷贝它的情况,这时将失去对私有对象的所有权,并造成内存泄漏:

private:
FooPrivate* const d;

这使您可以修改d指向的对象,但不能在初始化后修改d的值。

但是,您可能不希望所有成员变量都存在于私有数据对象中。对于经常使用的成员,将它们直接放入类中会更快,因为内联函数无法访问d指针数据。还要注意,尽管在d指针本身中已声明为公有,但d指针所涵盖的所有数据都是私有的。对于公有或受保护的访问,请同时提供set和get函数。例如

QString Foo::string() const
{
return d->s;
} void Foo::setString( const QString& s )
{
d->s = s;
}

也可以将d指针的私有类声明为嵌套的私有类。如果使用此技术,请记住,嵌套的私有类将继承包含的导出类的公有符号可见性。这将导致私有类的函数在动态库的符号表中被命名。您可以在嵌套私有类的实现中使用Q_DECL_HIDDEN来手动重新隐藏符号。从技术上讲,这是ABI变动,但不会影响KDE开发人员支持的公共ABI,因此私有符号错误可能重新隐藏,而不会发出进一步的警告。

故障排除

在没有d指针的情况下将新数据成员添加到类中

如果您没有自由的位标志,保留的变量并且也没有d指针,但是您必须添加一个新的私有成员变量,那么仍然存在一些可能性。如果您的类继承自QObject,则可以例如将其他数据放在一个特殊的子对象中,并通过遍历子对象列表来查找它们。您可以使用QObject::children()访问子列表。但是,更简便,通常更快的方法是使用哈希表存储对象与额外数据之间的映射。为此,Qt提供了一个基于指针的字典,称为QHash(或Qt3中的Templat::Qt3)。

在Foo类的实现中的基本技巧是:

  • 创建一个私有数据类FooPrivate。

  • 创建一个静态QHash<Foo *, FooPrivate *>。

  • 请注意,有些编译器/链接器(不幸的是,几乎所有的)都无法在动态库中创建静态对象。他们只是忘了调用构造函数。因此,您应该使用Q_GLOBAL_STATIC宏来创建和访问该对象:

    // BCI: Add a real d-pointer
    typedef QHash<Foo *, FooPrivate *> FooPrivateHash;
    Q_GLOBAL_STATIC(FooPrivateHash, d_func)
    static FooPrivate *d(const Foo *foo)
    {
    FooPrivate *ret = d_func()->value(foo);
    if ( ! ret ) {
    ret = new FooPrivate;
    d_func()->insert(foo, ret);
    }
    return ret;
    }
    static void delete_d(const Foo *foo)
    {
    FooPrivate *ret = d_func()->value(foo);
    delete ret;
    d_func()->remove(foo);
    }
  • 现在,您可以像以前的代码一样简单地在类中使用d指针,只需调用d(this)即可。例如:

    d(this)->m1 = 5;
  • 在析构函数中添加一行:

    delete_d(this);
  • 不要忘记添加一个BCI注释,以便可以在库的下一版本中删除该hack。

  • 不要忘记在下一个类中添加d指针。

添加已重新实现的虚函数

正如已经说明的,你可以安全的重新实现定义在其中一个基类中的虚函数,如果程序链接到先前版本的库,且该库调用了基类中的实现而不是派生类中的实现,是安全的。这是因为如果编译器可以确定要调用哪个虚函数,则有时会直接调用该虚函数。例如,如果您有

void C::foo()
{
B::foo();
}

那么B::foo()直接被调用。如果类B继承自实现了foo()函数的类A,而B本身未重新实现,则 C::foo() 实际上将调用A::foo()。如果该库的较新版本添加了B::foo(),则C::foo() 仅在重新编译后才调用B::foo() 。

另一个更常见的示例是:

B b;		// B derives from A
b.foo();

那么对foo()的调用将不会使用虚拟表。这意味着如果库中以前不存在B::foo(),但现在存在了,则使用较早版本库进行编译的代码仍将调用A::foo()。

如果不能保证无需重新编译就能继续工作,请将函数功能从A::foo()移至新的受保护函数A::foo2(),并使用以下代码:

void A::foo()
{
if( B* b = dynamic_cast< B* >( this ))
b->B::foo(); // B:: is important
else
foo2();
}
void B::foo()
{
// added functionality
A::foo2(); // call base function with real functionality
}

类型B(或继承)的对象对A::foo()的所有调用将导致调用B::foo()。唯一无法正常工作的情况是对A::foo()的调用,该调用显式指定了A::foo(),但B::foo()则调用了A::foo2(),其他地方别这样做。

使用新类

一种相对简单的“扩展”类的方法是编写一个替换类,该替换类还将包括新功能(可能从旧类继承代码以重复利用)。当然,这需要使用该库来适应和重新编译应用程序,因此这种方法不可能用来修复或扩展类的功能,该类是应用程序编译用的旧版本库中的类。 但是,特别是对于小型的和/或性能至关重要的类,编写它们可能会更简单,而不必确保它们将来会易于扩展;如果以后需要,可编写一个新的替代类,以提供新的功能或更好的性能。

向叶节点类添加新的虚函数

这种技术是使用新类的一种情况,这对向类中添加新的虚函数有帮助,该类必须保持二进制兼容性,而该类的继承类没必要继续保持二进制兼容性(即所有的继承类都在应用程序中)。在这种情况下,可以添加一个继承自原始类的新类,并将其添加进来。当然,使用新功能的应用程序必须进行修改以使用新类。

class A {
public:
virtual void foo();
};
class B : public A { // newly added class
public:
virtual void bar(); // newly added virtual function
};
void A::foo()
{
// here it's needed to call a new virtual function
if( B* this2 = dynamic_cast< B* >( this ))
this2->bar();
}

当还有其他的继承类也必须保持二进制兼容性时,则无法使用此技术,因为它们不得不从新类继承。

使用信号代替虚功能

Qt的信号和槽设计由Q_OBJECT宏创建的特殊的虚函数调用,它存在于从QObject继承的每个类中。因此,添加新的信号和槽不会影响二进制兼容性,并且可以使用信号/槽机制来模拟虚函数。

class A : public QObject {
Q_OBJECT
public:
A();
virtual void foo();
signals:
void bar( int* ); // added new "virtual" function
protected slots:
// implementation of the virtual function in A
void barslot( int* );
}; A::A()
{
connect(this, SIGNAL( bar(int*)), this, SLOT( barslot(int*)));
} void A::foo()
{
int ret;
emit bar( &ret );
} void A::barslot( int* ret )
{
*ret = 10;
}

函数bar()的作用类似于虚函数,barslot()实现了函数的实际功能。由于信号的返回值为void,因此必须使用参数来返回数据。 由于只有一个槽函数连接从槽中返回数据的信号,因此这种方式可以正常工作。 注意,要使Qt4起作用,连接类型必须为Qt::DirectConnection。

如果继承类要重新实现bar()的功能,则它必须提供自己的槽函数:

class B : public A {
Q_OBJECT
public:
B();
protected slots: // necessary to specify as a slot again
void barslot( int* ); // reimplemented functionality of bar()
}; B::B()
{
disconnect(this, SIGNAL(bar(int*)), this, SLOT(barslot(int*)));
connect(this, SIGNAL(bar(int*)), this, SLOT(barslot(int*)));
} void B::barslot( int* ret )
{
*ret = 20;
}

现在,B::barslot()将像重新实现虚函数A::bar()一样。请注意,有必要再次将barlot()指定为B中的槽,并且在构造函数中,有必要先断开连接,然后再次连接,这将断开A::barslot()并连接B::barslot() 。

注意:可以通过实现虚槽函数来实现相同目的。


原创造福大家,共享改变世界

献出一片爱心,温暖作者心灵


qt creator源码全方面分析(2-1-1)的更多相关文章

  1. qt creator源码全方面分析(3-3)

    目录 qtcreatordata.pri 定义stripStaticBase替换函数 设置自定义编译和安装 QMAKE_EXTRA_COMPILERS Adding Compilers 示例1 示例2 ...

  2. qt creator源码全方面分析(3-5)

    目录 qtcreatorlibrary.pri 使用实例 上半部 下半部 结果 qtcreatorlibrary.pri 上一章节,我们介绍了src.pro,这里乘此机会,把src目录下的所有项目文件 ...

  3. qt creator源码全方面分析(0)

    本人主攻C++和Qt. 上两天刚研究完Qt install framework(IFW)应用程序安装框架. google没发现有正儿八经的官方文档的翻译,我就进行了翻译哈!! 系列文章具体见:http ...

  4. qt creator源码全方面分析(4-0)

    Qt系统 Qt Creator源码是在Qt对象和框架基础下写的,因此,阅读Qt Creator源码,你首先对Qt得有一定的了解. Qt Core Qt Core特征: The Meta-Object ...

  5. qt creator源码全方面分析(4-2)

    目录 global头文件 global.h xx.h global头文件 插件的本质就是动态链接库,对于库,需要导出符号,供用户导入使用.在qt creator的源码中,存在固定的导入导出模式. gl ...

  6. qt creator源码全方面分析(4-5)

    目录 Qt中的字符串 QLatinString 详细介绍 源码 小结 QStringLiteral(str) 详细介绍 源码 小结 Qt中的字符串 Qt中处理字符串最常用的肯定是QString,但是在 ...

  7. qt creator源码全方面分析(4-6)

    目录 Qt插件基础 Qt插件基础 我们知道Qt Creator源码是基于插件架构的,那么我们先来介绍下插件基础知识. 相关内容如下: How to Create Qt Plugins [ - Defi ...

  8. qt creator源码全方面分析(3-2)

    目录 qtcreator.pri 判断重复包含 定义版本信息 VERSION 定义IDE名称 启用C++14 CONFIG 自定义函数 Replace Functions Test Functions ...

  9. qt creator源码全方面分析(2-7)

    目录 Completing Code 补全代码片段 编辑代码片段 添加和编辑片段 删除片段 重置片段 补全Nim代码 Completing Code 在编写代码时,Qt Creator建议使用属性,I ...

  10. qt creator源码全方面分析(2-10-1)

    目录 Getting and Building Qt Creator 获取Qt 获取和构建Qt Creator Getting and Building Qt Creator 待办事项:应该对此进行扩 ...

随机推荐

  1. 20191024-2 Alpha阶段贡献分配

    此作要求参见: https://edu.cnblogs.com/campus/nenu/2019fall/homework/9858 要求1 每位组员的贡献分值 罗杨美慧:9 徐丽君:11 魏鑫:12 ...

  2. 「每日五分钟,玩转JVM」:指针压缩

    64位JVM和32位JVM 最初的时候,JVM是32位的,但是随着64位系统的兴起,JVM也迎来了从32位到64位的转换,32位的JVM对比64位的内存容量比较有限,但是我们使用64位虚拟机的同时,也 ...

  3. zabbix 4.04 安装文档 - 基于CentOS 7.6

    1    安装前准备: 1.1   安装JDK 卸载openjdk # rpm -qa | grep java # yum remove java-1.8.0-openjdk # yum remove ...

  4. 使用vue-baidu-map解析geojson

    这是后台给我的gejson: {"type":"FeatureCollection","features":[{"type&quo ...

  5. Queue and deque

    Queue : 队列 队列(Queue)是常用的数据结构,可以将队列看成特殊的线性表,队列限制了对线性表的访问方式:只能从线性表的一端添加(offer)元素,从另一端取出(poll)元素. 队列遵循先 ...

  6. hadoop配置环境变量

    hadoop安装包解压 tar -xvf hadoop-2.7.7.tar.gz 解压成功ll查看文件 配置环境变量 1. vi  /home/wj/hadoop-2.7.7/etc/hadoop/h ...

  7. JUnit 5和Selenium基础(一)

    Gradle.JUnit 5和Jupiter Selenium Selenium是一组支持浏览器自动化的工具,主要用于Web应用程序测试.Selenium的组件之一是Selenium WebDrive ...

  8. 【转】安卓开发经验分享:资源、UI、函数库、测试、构建一个都不能少

    本文由 ImportNew - 唐尤华 翻译自 gigavoice.如需转载本文,请先参见文章末尾处的转载要求. 除了高超的武艺,每位黑忍者还需要装备最好的武器.在软件开发的世界里,好的工具能让我们的 ...

  9. WIN10升级后输入法无法输入中文

    查看是否安装了中文输入法,可能在升级后用户文件出现问题. 在设置>语言.添加一下中文输入法.

  10. JS中Cookie、localStorage、sessionStorage三者的区别

    cookie:大小4k,一般由服务器生成,可设置失效时间,关闭浏览器后失效,与服务器通信时:每次都会携带HTTP头中,如果使用cookie保存过多数据会带来性能问题 localhostStorage: ...