二进制兼容
在上一章结尾处提到了二进制兼容的概念,这里先说说二进制兼容的问题。
为什么是二进制兼容
简单说,就是我的可执行程序调用你的动态库(so/dll),若动态库发生改动,我替换库文件后仍可以直接运行,这就是二进制兼容。若需要重新编译才能运行,就是二进制兼容。
为什么会二进制兼容
二进制不兼容的根本原因是升级动态库导致内存模型发生了改变。如原本类中有一个int属性a,升级之后在a前面添加了一个int属性b。升级后,类多占用了4个四节。如果调用者不重新编译,原来调用a的地方已经变成b了,自然就会出问题。增加接口也会造成这样的问题,此时必须重新编译链接可执行程序,以更新动态库在程序中的对象模型。
什么情况会导致二进制不兼容
简单来说,只要改变了对象的内存模型,就会导致二进制不兼容。例如:
增加虚函数或改变虚函数的声明顺序,这会导致虚函数表中的函数位置发生变化(在末尾增加是一种取巧行为,如果类被继承,也还是会出问题);
增加非静态属性;
调整非静态属性的声明顺序
例如,对于第三个版本,略做一点修改:
class NetworkV3 {
public:
virtual int Send(const std::string str) = 0;
virtual int Recv(std::string &str) = 0;
// 创建和销毁函数
static NetworkV3* New();
static void Delete(NetworkV3 *net);
};
这里,我将 Send 和 Recv 的声明顺序颠倒一下,若不重新编译,调用时就会出问题。我是在 CentOS 上使用 g++ 9.3 编译了,此时虽然没有发生崩溃,但明显有调用错误,即原来调用 Send 的地方改为调用 Recv 了,若参数不一致,则直接没有调用。只有重新编译后,程序才能正常运行。
隐藏子类---解决二进制兼容
可以通过一个隐藏的子类和友元的方式来解决二进制兼容的问题,其实就是 上一章 中两个版本(版本二和版本三)的结合。具体代码如下:
// network.h
// 版本4
class NetworkV4 {
public:
int Send(const std::string str);
int Recv(std::string &str);
// 创建和销毁函数
static NetworkV4* New();
static void Delete(NetworkV4 *net);
protected:
NetworkV4();
~NetworkV4();
};
// network.cpp
// 版本4
class NetworkV4Impl : public NetworkV4
{
// 友元,NetworkV4 中可以访问 NetworkV4Impl 的私有成员
friend class NetworkV4;
private:
// NetworkV4 的成员变量
std::string str_;
};
NetworkV4::NetworkV4() {}
NetworkV4::~NetworkV4() {}
int NetworkV4::Send(const std::string str) {
NetworkV4Impl *impl = (NetworkV4Impl*)this;
// TODO: 通过访问 impl 的成员变量,实现 NetworkV4::Send
impl->str_ = str;
std::cout << "NetworkV4::Send: " << impl->str_ << std::endl;
return impl->str_.size();
}
int NetworkV4::Recv(std::string &str) {
NetworkV4Impl *impl = (NetworkV4Impl*)this;
// TODO: 通过访问 impl 的成员变量,实现 NetworkV4::Recv
impl->str_ = "ok";
str = impl->str_;
std::cout << "NetworkV4::Recv: " << str << std::endl;
return str.size();
}
// 创建和销毁函数
NetworkV4* NetworkV4::New() {
return (new NetworkV4Impl());
}
void NetworkV4::Delete(NetworkV4 *net) {
delete (NetworkV4Impl*)net;
}
NetworkV4的接口只有成员函数(非虚函数),无成员变量,但构造函数和析构函数必须定义为protected,防止外部创建对象,要使用静态方法创建对象。
只是有一点看着比较奇怪,为啥 NetworkV4Impl 一定要继承 NetworkV4 ,不继承是否可以。很显然的可以的,但如果不继承,就需要定义一个 NetworkV4Impl 全局变量,用于在 NetworkV4 中调用,而这与 NetworkV2 的方法几乎是一致的(NetworkV2 中使用的是成员变量)。但使用全局变量就有悖于 C++ 封装的特性,之所以搞这么复杂,还是为了封装得好用一些。
个人觉得这一种方法过于复杂了一点,没有上一章中的两种方法直观,这里就权当做个参考吧。
参考资料
C++接口工程实践:有哪些实现方法?
C++二进制兼容