设计 C++ 接口文件的小技巧之 PIMPL

这篇具有很好参考价值的文章主要介绍了设计 C++ 接口文件的小技巧之 PIMPL。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

C++ 里面有一些惯用法(idioms),如 RAII,PIMPL,copy-swap、CRTP、SFINAE 等。今天要说的是 PIMPL,即 Pointer To Implementation,指向实现的指针。

问题描述

在实际的项目中,经常需要定义和第三方/供应商的 C++ 接口。假如有这样一个接口文件:

MyInterface.h

#include <string>
#include <list>
#include "dds.h"

class MyInterface {
   public:
    int publicApi1();
    int publicApi2();

   private:
    int privateMethod1();
    int privateMethod2();
    int privateMethod3();

   private:
    std::string name_;
    std::list<int> list_;
    DDSDomainPariciant dp_;
    DDSTopic topic_;
    DDSDataWriter dw_;
};

该接口头文件存在以下问题:

  • 暴露了 MyInterface 内部实现。所有的 private/protected 的方法、成员变量都暴露给接口的使用者
  • 由此带来的另一个问题是接口不稳定。假如我们修改类的内部实现,即使不改变 public 接口,接口的使用者也需要跟着更新头文件:
    • 比如 list_ 成员之前用的是 std::list 容器,现在打算改用 std::vector 容器
    • 再比如,之前有 3 个 private 方法,现在重构实现部分,拆成更多的小函数
  • 增加了使用者的依赖。接口的使用者想要使用上述头文件,必须要 #include "dds.h" 这个文件,而 "dds.h" 通常又会 #include 很多其他文件。最终的结果往往是要向接口的使用者提供很多额外的头文件。如果将来重构,不用 DDS,改用 SOME/IP 或其他中间件,接口的使用者也要跟着改变。不仅如此,为 private 成员而额外 #include 的头文件也会增加编译时间

解决方案 —— PIMPL

PIMPL 就是 C++ 里专门用来解决这些问题的惯用法。PIMPL 将 MyInterface 类的具体实现(private/protected 方法、成员)转移到另外一个嵌套类 Impl 中,然后利用前向声明(forward declaration)声明 Impl,并在原有的 MyInterface 接口类中增加一个指向 Impl 对象的指针。再次强调,在 MyInterface 中的 Impl 仅仅是一个前向声明,MyInterface 类只知道有 Impl 这么个类,但是对 Impl 有哪些方法、哪些成员变量一无所知,因此能做的事情非常有限(声明一个指向该类的指针就是其中之一)。而这恰恰就是 PIMPL 将接口和实现解耦的关键所在。

应用 PIMPL 后的 MyInterface.h 文件:

class MyInterface {
   public:
    MyInterface();
    ~MyInterface();
    
    int publicApi1();
    int publicApi2();

   private:
    struct Impl;
    Impl* impl_;
};

现在 MyInterface.h 接口文件变得非常清爽。原本在 MyInterface.h 中的那些依赖、实现细节,现在通通转移到了 MyInterface.cpp 内部,对接口的使用者彻底隐藏,降低使用者的依赖,提高接口稳定性

MyInterface.cpp

#include <string>
#include <list>
#include "dds.h"

struct MyInterface::Impl {
    int publicApi1();
    int publicApi2(int i);

    int privateMethod1();
    int privateMethod2();
    int privateMethod3();

    std::string name_;
    std::list<int> list_;
    DDSDomainPariciant dp_;
    DDSTopic topic_;
    DDSDataWriter dw_;
};

MyInterface::MyInterface() 
    : pimpl_(new Impl()) {}

MyInterface::~MyInterface() {
    delete pimpl_;
}

int MyInterface::publicApi1() {
    impl_->publicApi1();
}
int MyInterface::publicApi2(int i) {
    impl_->publicApi2(i);
}

// 其他 MyInterface::Impl 类的方法实现
// 原本 MyInterface 中的逻辑挪到 MyInterface::Impl 中
int MyInterface::Impl::publicApi1() {...}

可以看到,MyInterface 类的实现本身只是单纯地将请求委托/转发给 MyInterface::Impl 的同名方法。对于参数的传递,也可以适当使用 std::move 提升效率(关于 std::move 今后也可以展开说说)。

也可以把嵌套类 MyInterface::Impl 放到单独 MyInterfaceImpl.h/cpp 中,如此一来 MyInterface.cpp 就会变得非常简洁,就像下面这样:

MyInterface.cpp

#include "MyInterface.h"
#include "MyInterfaceImpl.h"

MyInterface::MyInterface() 
    : pimpl_(new Impl()) {}

MyInterface::~MyInterface() {
    delete pimpl_;
}

int MyInterface::publicApi1() {
    return impl_->publicApi1();
}

int MyInterface::publicApi2(int i) {
    return impl_->publicApi2(i);
}

MyInterfaceImpl.h

#include <string>
#include <list>
#include "dds.h"

struct MyInterface::Impl {
    int publicApi1();
    int publicApi2(int i);

    int privateMethod1();
    int privateMethod2();
    int privateMethod3();

    std::string name_;
    std::list<int> list_;
    DDSDomainPariciant dp_;
    DDSTopic topic_;
    DDSDataWriter dw_;
};

MyInterfaceImpl.cpp

#include "MyInterfaceImpl.h"

int MyInterface::Impl::publicApi1() {
    // ...
}

// 其他 MyInterface::Impl 类的方法实现

注意不要在 MyInterface.h 中 #include "MyInterfaceImpl.h",否则就前功尽弃了。

现代 C++ 中的 PIMPL

以上是传统 C++ 中的 PIMPL 的实现,现代 C++ 应尽量避免使用裸指针,而使用智能指针。具体的原因见这篇文章「裸指针七宗罪」。

MyInterface 拥有 Impl 对象的专属所有权,unique_ptr 是最自然的选择。如果直接将上述的裸指针替换成 unique_ptr:

#include <memory>

class MyInterface {
   public:
    MyInterface();
    int publicApi1();
    int publicApi2();

   private:
    struct Impl;
    std::unique_ptr<Impl> impl_;
};

// main.cpp
int main() {
    MyInterface if;
}

会看到这样的报错:

/opt/compiler-explorer/gcc-13.1.0/include/c++/13.1.0/bits/unique_ptr.h: In instantiation of 'constexpr void std::default_delete<_Tp>::operator()(_Tp*) const [with _Tp = MyInterface::Impl]':
/opt/compiler-explorer/gcc-13.1.0/include/c++/13.1.0/bits/unique_ptr.h:404:17:   required from 'constexpr std::unique_ptr<_Tp, _Dp>::~unique_ptr() [with _Tp = MyInterface::Impl; _Dp = std::default_delete<MyInterface::Impl>]'
<source>:118:7:   required from here
/opt/compiler-explorer/gcc-13.1.0/include/c++/13.1.0/bits/unique_ptr.h:97:23: error: invalid application of 'sizeof' to incomplete type 'MyInterface::Impl'
   97 |         static_assert(sizeof(_Tp)>0,
      |                       ^~~~~~~~~~~

问题出在哪里呢?

问题就出在 MyInterface 的析构函数。在没有显式声明析构函数的情况下,编译器会自动合成一个隐式内联的析构函数(编译器在什么条件下,自动合成哪些函数也有不少学问,后面会单独发一篇),等效代码如下:

class MyInterface {
   public:
    MyInterface();
    ~MyInterface(){} // 是实现,不是声明!
    int publicApi1();
    int publicApi2();

   private:
    struct Impl;
    std::unique_ptr<Impl> impl_;
};

在 MyInterface.h 中,编译器自动合成的析构函数会进行以下操作:

  1. 执行空的析构函数体
  2. 按照构造的相反顺序,依次销毁 MyInterface 的成员
  3. 销毁 unique_ptr<Impl> impl_ 成员
  4. 调用 unique_ptr 的析构函数
  5. unique_ptr 的析构函数调用默认的删除器(delete),删除指向的 Impl 对象

我们所看到报错,就出在第 5 步。unique_ptr 的实现代码在删除前,会进行 static_assert(sizeof(_Tp)>0 断言,而编译器执行该断言的时候,Impl 还是一个不完整类型(Incomplete Type)。因为编译器此时只看到了 MyInterface::Impl 的前向声明,还没有看到定义,不知道 Impl 有哪些成员,也不知 Impl 类占用多大内存,所以在进行 sizeof(Impl) 的时候报错。

知道了背后的原理,解决起来也很简单,就是保证在 MyInterface 析构函数实现的地方,能看到 Impl 类的定义即可:

MyInterface.h

#include <memory>

class MyInterface {
   public:
    MyInterface();
    ~MyInterface();  // 使用 unique_ptr 的关键:只声明,不实现!
    int publicApi1();
    int publicApi2();

   private:
    struct Impl;
    std::unique_ptr<Impl> impl_;
};

MyInterface.cpp

#include <memory>
#include "MyInterface.h"
#include "MyInterfaceImpl.h"

MyInterface::MyInterface()
    : pImpl_(std::make_unique<Impl>()) {}

MyInterface::~MyInterface() = default;

int MyInterface::publicApi1() {
    return impl_->publicApi1();
}
int MyInterface::publicApi2(int i) {
    return impl_->publicApi2(i);
}

这样,一个正确的 PIMPL 就搞定啦!虽然 PIMPL 多了一层封装,稍微增加了一点点复杂度,但我认为这么做是绝对的利大于弊。以一个我曾参与的项目为例,在将近一年的时间里,实现库更新了很多版,但是接口文件从释放以来一直没变过,大大减少了和第三方/供应商的沟通、调试成本。

最后,留一个思考题:为什么将 unique_ptr 换成 shared_ptr 不会遇到上面的 static_assert(sizeof(_Tp)>0 编译错误?如果你能解释其中的原因,那说明你对 shared_ptr、unique_ptr 的理解相当深入了👏

原文地址:https://www.cnblogs.com/tengzijian/p/17473602.html文章来源地址https://www.toymoban.com/news/detail-488325.html

到了这里,关于设计 C++ 接口文件的小技巧之 PIMPL的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • JS好用的小技巧

    生成数组 当你需要要生成一个 0-99 的数组  第一种  第二种 打乱数组 当你有一个数组,你需要打乱这个数组的排序 数组去重 当你需要将数组中的所有重复的元素只保留一个 多数组取交集 当你需要取多个数组中的交集 查找最大值索引 但你需要找到一个数组中的最大值的索

    2024年02月14日
    浏览(34)
  • Postman生成代码的小技巧

    你还在使用postman吗?你还是一条条复制参数吗?你还是手动录入数据吗?对于一些不经常使用postman的人来说,这个小技巧可以帮助你导入请求,以及转换成开发语言。 1 抓包接口 以CSDN热榜为例,直接F12,查看请求,找到对应接口 2 复制cURL 右击接口 Copy--- Copy as cURL(bash) ,不

    2024年02月05日
    浏览(33)
  • 24个写出漂亮代码的小技巧

    这篇文章我会总结一些实用的有助于提高代码质量的建议,内容较多,建议收藏! 内容概览: 注解、反射和动态代理是 Java 语言中的利器,使用得当的话,可以大大简化代码编写,并提高代码的可读性、可维护性和可扩展性。 我们可以利用 注解 + 反射 和 注解+动态代理 来

    2024年02月05日
    浏览(38)
  • 记录--9个封装Vue组件的小技巧

    组件是前端框架的基本构建块。把它们设计得更好会使我们的应用程序更容易改变和理解。在这节课中,分享一下在过去几年中工作中学到的 9 个技巧。 在创建一个组件之前,看看它是为了可重用性和为某些UI添加一个状态,还是仅仅为了组织和划分代码。 如果是后者,那么

    2024年02月04日
    浏览(49)
  • vue3中一些简单的小技巧

    最近在学习vue3+vite的时候学习到的一些小技巧,现在记录一下 学习vue3+vite中看到了一些小技巧,这个小技巧可以在写代码时更加的顺畅,更加的丝滑。 在写vue3中,有一个语法糖大家一定很清楚,那就是setup,但使用setup语法带来的一个问题就是无法自己设置name,而当我们使

    2023年04月09日
    浏览(36)
  • c 语言的小技巧之 输入 %*s

    %*s是格式化字符串的一部分,用于在输入过程中跳过一个字符串而不将其存储到变量中。这在处理需要跳过特定部分的输入时非常有用。 具体来说,%*s的工作方式如下: %*s:表示忽略一个字符串。*用于指定一个可选的字段宽度,但是在这种情况下,字段宽度没有实际的作用

    2024年02月11日
    浏览(29)
  • 9 个让你的 Python 代码更快的小技巧

    哈喽大家好,我是咸鱼 我们经常听到 “Python 太慢了”,“Python 性能不行”这样的观点。但是,只要掌握一些编程技巧,就能大幅提升 Python 的运行速度。 今天就让我们一起来看下让 Python 性能更高的 9 个小技巧 原文链接: https://medium.com/techtofreedom/9-fabulous-python-tricks-that-m

    2024年02月03日
    浏览(51)
  • 记录uniapp 高度铺满全屏的小技巧

    在uniapp中,高度使用heiht:100vh,h5的屏幕会多出一些高度,导致可以上下滑动 解决方式如下 在app.vue中设置一个公共样式 在需要高度铺满全屏的页面的最外层的view绑定类名page,样式为 就可以实现页面高度铺满全屏了

    2024年02月11日
    浏览(35)
  • 在Unity中一些Loading界面制作的小技巧

    目录 1.使用自己的图片制作游戏开始、加载界面。 2.制作加载进度条并且实现场景跳转 3.制作简单计时器并且实现场景跳转 添加Canvas,image,Rawimage. 将图片导入到Unity中,可以创建一个文件夹保存它们,直接拖拽进来即可。(图片拖拽到Rawimage上,就会显示图片) 首先先添加场

    2024年01月16日
    浏览(42)
  • 二叉树 - 堆 | 数据结构中的小技巧大作用

    📷 江池俊: 个人主页 🔥个人专栏: ✅数据结构冒险记 ✅C语言进阶之路 🌅 有航道的人,再渺小也不会迷途。 堆(Heap) 是计算机科学中一类特殊的数据结构的统称。 堆通常是一个可以被看做一棵完全二叉树的数组。 需要注意的是这里的堆和操作系统虚拟进程地址空间中的

    2024年01月22日
    浏览(42)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包