【发布时间】:2013-03-16 03:44:40
【问题描述】:
我正在学习 PIMPL 成语。它的优点之一是二进制兼容性。我想知道二进制兼容性的优点是什么。谢谢!
【问题讨论】:
-
为索赔提供参考
标签: c++ pimpl-idiom binary-compatibility
我正在学习 PIMPL 成语。它的优点之一是二进制兼容性。我想知道二进制兼容性的优点是什么。谢谢!
【问题讨论】:
标签: c++ pimpl-idiom binary-compatibility
它避免了Fragile Binary Interface Problem。它是这样的:
程序使用库。
用户升级库。升级改变了库的二进制接口。
程序现在在重新编译之前无法运行,因为它是根据旧的二进制接口构建的。
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 文件)链接的。注意2:还有另一种方法可以在不使用 PIMPL 的情况下解决相同的问题 - 命名空间的 ABI 版本控制。
【讨论】:
PIMPL 习惯用法的优势与其说是二进制兼容性,不如说是在更改实现甚至类的布局时减少了重新编译的需要。例如,如果您向一个类添加一个新数据成员,这会改变该类的布局,您通常需要重新编译该类的所有客户端,但如果您使用 PIMPL 习惯用法,则不需要。
二进制兼容性更多的是与多个编译器(和编译器版本)兼容,而在 C++ 中做到这一点的唯一方法是使用由不向客户端公开的类实现的接口(抽象类)。这是因为所有编译器都以相同的方式实现抽象类的 vtable 布局。许多 API,例如 DirectX API,都是以这种方式公开的,因此它们可以与任何编译器一起使用。
【讨论】: