我正在学习 PIMPL 成语。它的优点之一是二进制兼容性。我想知道二进制兼容性的优点是什么。谢谢!
3 回答
它避免了脆弱的二进制接口问题。它是这样的:
程序使用库。
用户升级库。升级更改了库的二进制接口中的某些内容。
程序现在在重新编译之前无法运行,因为它是根据旧的二进制接口构建的。
PIMPL 习惯用法的优点之一是它允许您将通常属于类的公共接口的一部分的东西移动到其私有接口中(实际上,移动到私有类的接口中)。您可以在不破坏二进制兼容性的情况下更改私有接口。
v1.0
libMagic
让我们考虑 v1.0库中的以下类
//MagicNumber.h
struct MagicNumber {
MagicNumber();
int get();
int id;
}
//MagicNumber.cpp
int MagicNumber::get() {
return 42;
}
申请代码:
void foo() {
MagicNumber m;
int i = 27;
std::cout << m.get() + i << '\n';
}
上述应用程序代码通过动态链接编译时libMagic.so
,foo
函数编译如下
foo:
Allocate 4 bytes space in stack for m
Allocate 4 bytes space in stack for i and write 27 in it
Call MagicNumber::get //This address is resolved on application startup.
... //Rest of processing
v1.0.1
现在,当 libMagic 发布新版本 v1.0.1 时,实现方式有以下变化,但头文件没有变化
//MagicNumber.cpp
int MagicNumber::get() {
return call_real_magic_number_fn();
}
应用程序不必重新编译,因此不需要更新。它将自动调用具有新实现的更新版本。
v1.1.0 - 二进制不兼容
可以说库(v1.1.0)还有以下更改。
//MagicNumber.h
struct MagicNumber {
MagicNumber();
int get();
int id;
int cache; //Note: New member
}
//MagicNumber.cpp
int MagicNumber::get() {
if(cache != 0) return cache;
cache = call_real_magic_number_fn();
return cache;
}
现在,编译后的foo
函数不会为添加的新成员分配空间。该库破坏了二进制兼容性。
foo:
Allocate 4 bytes space in stack for m //4 bytes is not enough for m
Allocate 4 bytes space in stack for i and write 27 in it.
Call MagicNumber::get //This address is resolved on application startup.
... //Rest of processing
发生的是未定义的行为。可能i=27
会写入缓存变量并MagicNumber::get
返回 27。但任何事情都可能发生。
如果libMagic
使用了 PIMPL 成语,所有成员变量都将属于MagicNumberImpl
其大小不会暴露给应用程序代码的类。因此库作者可以在以后版本的库中添加新成员,而不会破坏二进制兼容性。
struct MagicNumberImpl;
struct MagicNumber {
MagicNumber();
private:
MagicNumberImpl* impl;
}
上面的类定义在新版本中不会改变,当新成员添加到类中时指针的大小不会改变。
注意:仅在以下情况下才需要考虑二进制兼容性
- 该库使用动态链接(例如
.so
linux 中的文件)进行链接。 - 该库无需重新编译应用程序代码即可更新到新版本。如果库和二进制文件在同一个项目中 - 您的构建系统将自动重新编译和更新两者。因此,无需为此或 PIMPL 烦恼。
注意 2:还有另一种方法可以在不使用 PIMPL - ABI 命名空间版本控制的情况下解决相同的问题。
PIMPL 习惯用法的优势与其说是二进制兼容性,不如说是在更改实现甚至类的布局时减少了重新编译的需要。例如,如果您向一个类添加一个新的数据成员,这会改变该类的布局,并且您通常需要重新编译该类的所有客户端,但如果您使用 PIMPL 习惯用法,则不需要。
二进制兼容性更多的是与多个编译器(和编译器版本)兼容,而在 C++ 中做到这一点的唯一方法是使用由不向客户端公开的类实现的接口(抽象类)。这是因为所有编译器都以相同的方式实现抽象类的 vtable 布局。许多 API,例如 DirectX API,都是以这种方式公开的,因此它们可以与任何编译器一起使用。