C++ IMPL模式解析(下)

二进制兼容

在上一章结尾处提到了二进制兼容的概念,这里先说说二进制兼容的问题。

为什么是二进制兼容

简单说,就是我的可执行程序调用你的动态库(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++二进制兼容

posted @ 2022-12-05 11:27  sgggr  阅读(411)  评论(0编辑  收藏  举报