C++基础:Pimpl设计模式的实现

2024/11/14:

在实现C++17的Any类时偶然接触到了嵌套类的实现方法以及Pimpl设计模式,遂记录。

PIMPL ( Private Implementation 或 Pointer to Implementation )是通过一个私有的成员指针,将指针所指向的类的内部实现数据进行隐藏。

通俗点来说,就是因为各种各样的原因(如:你不愿意让别人看到你的具体实现,你希望让头文件看起来更清爽)需要将底层的实现封装起来,只将API暴露出来。

举个例子:

cpp 复制代码
class Line
{
public:
    Line(int x1, int y1, int x2, int y2)
        : _p1(x1, y1)
        , _p2(x2, y2)
    {}
    void showLine()
    {
        std::cout << "Line:" << std::endl;
        _p1.showPoint();
        _p2.showPoint();
    }
private:
    // 嵌套类放入private,专门为line服务
    class Point
    {
    public:
        ~Point() = default;
        Point(int a, int b)
                : _x(a)
                , _y(b)
        {}
        void showPoint() const
        {
            std::cout << "Point:" << std::endl;
            std::cout << _x << " " << _y << std::endl;
        }
    private:
        int _x, _y;

    };
    Point _p1, _p2;
};

Line类对于那些只关注函数功能的用户来说可能会带来一定阅读成本。所以我们使用Pimpl设计模式,在这个类上面套一层壳子,将它隐藏起来。

具体如下面代码所示:

cpp 复制代码
// Line.h
#ifndef LINE_H
#define LINE_H
#include <iostream>
#endif
class OutLine
{
public:
    OutLine(int x1, int y1, int x2, int y2);
    ~OutLine();
    void showOutLine();
private:
    // 嵌套类的前向声明
    class Line;
    Line * _Lineptr;
};
cpp 复制代码
// Line.cpp
#include "Line.h"
class OutLine::Line
{
public:
    Line(int x1, int y1, int x2, int y2)
            : _p1(x1, y1)
            , _p2(x2, y2)
    {}
    void showLine()
    {
        std::cout << "Line:" << std::endl;
        _p1.showPoint();
        _p2.showPoint();
    }
private:
    // 嵌套类放入private,专门为line服务
    class Point
    {
    public:
        ~Point() = default;
        Point(int a, int b)
                : _x(a)
                , _y(b)
        {}
        void showPoint() const
        {
            std::cout << "Point:" << std::endl;
            std::cout << _x << " " << _y << std::endl;
        }
    private:
        int _x, _y;

    };
    Point _p1, _p2;
};

OutLine::OutLine(int x1, int y1, int x2, int y2)
: _Lineptr(new Line(x1, y1, x2, y2))
{
    std::cout << "create outLine" << std::endl;
}

OutLine::~OutLine()
{
    std::cout << "delete outLine" << std::endl;
    delete _Lineptr;
    _Lineptr = nullptr;
}

void OutLine::showOutLine() {
    if (_Lineptr) {
        _Lineptr->showLine();
    }
}

可以看到,在Line.h文件中,我们使用了OutLine类作为套在Line类上的一层壳,并且成功隐藏了底层较为复杂的实现。

下列代码用于测试:

cpp 复制代码
// main.cpp
int main()
{
    OutLine l(1, 4, 2, 3);
    l.showOutLine();
}

Pimpl设计模式主要用于将代码打包成头文件和库交给第三方使用。显而易见的,在类上再套一个类增加了一点点开销,但是相对于好处而言,这点代价是可以接受的。

除了信息的隐藏,还有几点好处:

1、只要头文件不变,可以实现对已实现的函数(参数列表不能变)功能的修改和优化。在修改之后,只需要替换动态库而不需要替换头文件。即可以实现库的平滑升级

这一点是显而易见的,比如,当我们运行上面的测试代码时,会有以下输出:

create outLine

Line:

Point:

1 4

Point:

2 3

delete outLine

可能我们觉得输出左对齐不是很美观,想美化一下输出,所以我们在Line.cpp文件中改一下Point类的输出函数:

cpp 复制代码
void showPoint() const
{
      std::cout << "\tPoint: " <<  _x << " " << _y << std::endl;
}

再运行一下测试代码,得到输出:

create outLine

Line:

Point: 1 4

Point: 2 3

delete outLine

在这个修改过程中,并没有改动Line.h头文件。

2、Pimpl最重要的功能:编译防火墙

这一点其实跟上一点是相关联的,见下图:

reference c++库文件头文件链接原理(全)

共享库(动态库)的代码是在可执行程序运行时才载入内存的,在编译过程中仅简单的引用,因此生成的可执行程序代码体积较小。

有些修改不会使头文件发生变化,只需要替换动态库(动态库是由cpp文件生成的),那么整个项目就不需要重新编译一遍。可以说这种设计模式完全发挥了动态库的优势。

可能有些人对这个编译的时间开销没什么概念,这里举个不怎么恰当的例子:有些游戏只是修了个bug,可能就要让整个游戏重新下载。而注意这方面优化的游戏,可能下个几mb的补丁就结束了。

前面的思路可能有点乱,这里总结一下采用Pimpl模式的优势与动态库的关系:

当动态库更新时,通常情况下不需要重新编译可执行文件。因为在编译生成可执行文件时,链接器并不会将动态库的代码直接复制到可执行文件中,而只是记录了对该动态库的引用。程序在执行时会根据这些引用加载相应的动态库。如果该动态库已被加载,程序则不会重复加载,从而节省内存资源。

然而,如果动态库的接口发生变化(例如函数的参数类型或返回值类型发生改变,或者函数被删除或重命名),那么可能需要重新编译可执行文件。在这种情况下,原有的可执行文件将无法正确调用动态库中的函数,可能会导致错误或异常。因此,Pimpl设计模式的编译防火墙优势就是建立在不去动这些接口的基础上的,这就要求项目有一个良好的设计思路,以尽量减少后期对接口的修改。

可见Pimpl设计模式与动态库的匹配性。

相关推荐
一只小小汤圆4 分钟前
编译笔记:vs 中 正在从以下位置***加载符号 C# 中捕获C/C++抛出的异常
c++·c#
重生之绝世牛码1 小时前
Java设计模式 —— 【结构型模式】外观模式详解
java·大数据·开发语言·设计模式·设计原则·外观模式
shinelord明1 小时前
【再谈设计模式】享元模式~对象共享的优化妙手
开发语言·数据结构·算法·设计模式·软件工程
小俊俊的博客1 小时前
海康RGBD相机使用C++和Opencv采集图像记录
c++·opencv·海康·rgbd相机
_WndProc2 小时前
C++ 日志输出
开发语言·c++·算法
薄荷故人_2 小时前
从零开始的C++之旅——红黑树及其实现
数据结构·c++
m0_748240022 小时前
Chromium 中chrome.webRequest扩展接口定义c++
网络·c++·chrome
qq_433554542 小时前
C++ 面向对象编程:+号运算符重载,左移运算符重载
开发语言·c++
努力学习编程的伍大侠2 小时前
基础排序算法
数据结构·c++·算法
yuyanjingtao3 小时前
CCF-GESP 等级考试 2023年9月认证C++四级真题解析
c++·青少年编程·gesp·csp-j/s·编程等级考试