我们知道,当对头文件进行更改时,包括它的所有源都需要重新编译。在大型项目和库中,由于即使对实现进行了很小的更改,每个人都必须等待一段时间才能编译代码,这可能会导致构建时间问题。

解决此问题的一种方法是使用PImpl Idiom,它将实现隐藏在hearer中,并包括一个可立即编译的接口文件。

The PImpl Idiom (Pointer to IMPLementation) 是一种用于将实现与接口分离的技术。这项技术通过把类中的成员变量替换成指向一个实现类(或结构体)的opaque pointer,成员变量被放进单独的实现类中,然后通过该指针间接获取原来的成员变量。

其最大程度地减少了hearer暴露,并帮助程序员减少了构建依赖性。

class Widget {      // 在头文件“widget.h”中
public:Widget();...
private:std::string name;std::vector<double> data;Gadget g1, g2, g3;     // Gadget是某个用户定义的类型
};

因为Widget的成员变量有std::stringstd::vectorGadget,那么这些类型的头文件在Widget编译时必须出现,这意味Widget的用户必须要#include <string>,<vector>以及gadget.h

这些增加的头文件会增加Widget用户的编译时间,而且这使得用户依赖于这些头文件,即如果某个头文件的内容被改变了,Widget的用户就要重新编译。 标准库头文件和不会经常改变,但是“gadget.h”可能会经常修改。

在C++98中使用 Pimpl Idiom,让Widget的成员变量替换成一个指向结构体的原生指针,这个结构体只被声明,没有被实现

class Widget {     // 依然在头文件“widget.h”中
public:Widget();~Widget();...
private:struct Impl;    // 声明实现类Impl *pImpl;    // 声明指针指向实现类
};

因为Widget不再提起std::stringstd::vectorGadget类型,所以Widget的用户不再需要“#include”那些头文件了。那样加快了编译速度,也意味着当头文件内容改变时,Widget的用户不会受到影响。

一个被声明,却没定义的类型称为不完整类型(incomplete type)。 Widget::Impl就是这样的类型,不完整类型能做的事情很少,不过可以声明一个指针指向它们,Pimpl Idiom就是利用了这个特性。

Pimpl Idiom的第一部分是声明一个指向不完整类型的指针作为成员变量,第二部分是动态分配和回收一个装有原来成员变量的对象,分配和回收的代码要写在实现文件,例如,对于Widget,写在“Widget.cpp”中:

#include "widget.h"      // 在实现文件“widget.cpp”
#include "gadget.h"
#include <string>
#include <vector>`
struct Widget::Impl {   // 用原来对象的成员变量来定义实现类std::string name;std::vector<double> data;Gadget g1, g2, g3;
};`
Widget::Widget() : pImpl(new Impl) {}  // 为Widget对象动态分配成员变量
Widget::~Widget() { delete pImpl; }  // 销毁这个对象的成员变量

依赖已经从“widget.h”(它被所有Widget类的使用者包含,并且对他们可见)转移到“widget.cpp”(该文件只被Widget类的实现者包含,并只对它可见)。现在,即使gadget.h发生了任何改变,影响的也仅仅是“widget.cpp”而与“widget.h”无关,而“widget.h” 并不需要include “widget.cpp” 。 不过这个代码是动态分配的,需要在Widget的析构函数中回收分配的对象。

实例


实现方法:

  1. 创建一个单独的class(或struct)以实现
  2. 将所有header的私有成员放到这个类中
  3. 在头文件中定义一个实现类(Impl)
  4. 在头文件中,创建一个指向实现类的前向声明(指针)
  5. 定义一个析构函数和一个复制/赋值运算符

明确声明析构函数的原因是,在编译时,智能指针(std :: unique_ptr)检查类型定义中是否存在可见的析构函数,如果仅前向声明,则会引发编译错误。

Example:

  • 头文件中包含的类定义是该类的公共接口
  • 我们定义unique pointer(std::unique_ptr)而不是原始的指针,因为接口类型的对象负责对象的生存期
  • 由于std :: unique_ptr是完整类型,因此需要用户声明的析构函数和复制/赋值运算符才能使实现类完整
  • 从用户的角度来看,The pimpl approach是透明的。在内部,对IMPLementation结构所做的更改仅影响包含它的文件(User.cpp)。这意味着用户无需重新编译即可应用这些更改。
/* |INTERFACE| User.h file */#pragma once
#include <memory> // PImpl
#include <string>
using namespace std; class User {
public: // Constructor and Destructors ~User(); User(string name); // Asssignment Operator and Copy Constructor User(const User& other); User& operator=(User rhs); // Getter int getSalary(); // Setter void setSalary(int); private: // Internal implementation class class Impl; // Pointer to the internal implementation unique_ptr<Impl> pimpl;
};
/* |IMPLEMENTATION| User.cpp file */#include "User.h"
#include <iostream>
using namespace std; struct User::Impl { Impl(string name) : name(name){}; ~Impl(); void welcomeMessage() { cout << "Welcome, "<< name << endl; } string name; int salary = -1;
}; // Constructor connected with our Impl structure
User::User(string name) : pimpl(new Impl(name))
{ pimpl->welcomeMessage();
} // Default Constructor
User::~User() = default; // Assignment operator and Copy constructor User::User(const User& other) : pimpl(new Impl(*other.pimpl))
{
} User& User::operator=(User rhs)
{ swap(pimpl, rhs.pimpl); return *this;
} // Getter and setter
int User::getSalary()
{ return pimpl->salary;
} void User::setSalary(int salary)
{ pimpl->salary = salary; cout << "Salary set to "<< salary << endl;
}

PImpl的优点:

  • 二进制兼容性:二进制接口独立于私有字段。对实现进行更改不会使相关代码停滞不前。
  • 编译时间:由于只需要重建实现文件而不是每个客户端都重新编译其文件,因此编译时间减少了。
  • 数据隐藏:可以轻松隐藏某些内部细节,例如实现技术和用于实现公共接口的其他库。

PImpl的缺点:

  • 内存管理:由于分配的内存多于默认结构,因此内存使用量可能会增加,这对于嵌入式软件开发至关重要。
  • 维护工作:由于为了使用pimpl和附加的指针间接调用而增加了类,因此维护变得更加复杂(接口只能通过指针/引用使用)。
  • 继承:隐藏的实现(Hidden implementation)无法继承,尽管PImpl类可以

More example:

#include <iostream>
#include <memory>
#include <experimental/propagate_const>// interface (widget.h)
class widget {class impl;std::experimental::propagate_const<std::unique_ptr<impl>> pImpl;public:void draw() const; // public API that will be forwarded to the implementationvoid draw();bool shown() const { return true; } // public API that implementation has to callwidget(int);~widget(); // defined in the implementation file, where impl is a complete typewidget(widget&&); // defined in the implementation file// Note: calling draw() on moved-from object is UBwidget(const widget&) = delete;widget& operator=(widget&&); // defined in the implementation filewidget& operator=(const widget&) = delete;
};// implementation (widget.cpp)
class widget::impl {int n; // private datapublic:void draw(const widget& w) const {if(w.shown()) // this call to public member function requires the back-reference std::cout << "drawing a const widget " << n << '\n';}void draw(const widget& w) {if(w.shown())std::cout << "drawing a non-const widget " << n << '\n';}impl(int n) : n(n) {}
};
void widget::draw() const { pImpl->draw(*this); }
void widget::draw() { pImpl->draw(*this); }
widget::widget(int n) : pImpl{std::make_unique<impl>(n)} {}
widget::widget(widget&&) = default;
widget::~widget() = default;
widget& widget::operator=(widget&&) = default;// user (main.cpp)
int main()
{widget w(7);const widget w2(8);w.draw();w2.draw();
}

Reference:

  • https://en.cppreference.com/w/cpp/language/pimpl
  • https://www.geeksforgeeks.org/pimpl-idiom-in-c-with-examples/
  • Effective Modern C++ 条款22

pimple idiom相关推荐

  1. Pimple - 一个简单的 PHP 依赖注入容器

    链接 官网 WebSite GitHub - Pimple 这是 Pimple 3.x 的文档.如果你正在使用 Pimple 1.x ,请查看 Pimple 1.x 文档. 阅读 Pimple 1.x ...

  2. Pimple相关的源码

    已经有了非常好的Pimple的相关解析,建议先看下: Pimple - 一个简单的 PHP 依赖注入容器 读 PHP - Pimple 源码笔记(上) 读 PHP - Pimple 源码笔记(下) 这 ...

  3. Item 6: Use the explicitly typed initializer idiom when auto deduces undesired types.

    Item 6: Use the explicitly typed initializer idiom when auto deduces undesired types. 这次是对 Effective ...

  4. 单例模式的两种实现方式对比:DCL (double check idiom)双重检查 和 lazy initialization holder class(静态内部类)...

    首先这两种方式都是延迟初始化机制,就是当要用到的时候再去初始化. 但是Effective Java书中说过:除非绝对必要,否则就不要这么做. 1. DCL (double checked lockin ...

  5. The RAII Programming Idiom

    https://www.hackcraft.net/raii/ https://en.wikipedia.org/wiki/Resource_Acquisition_Is_Initialization ...

  6. idiom的学习笔记(一)、三栏布局

    三栏布局左右固定,中间自适应是网页中常用到的,实现这种布局的方式有很多种,这里我主要写五种.他们分别是浮动.定位.表格.flexBox.网格. 在这里也感谢一些老师在网上发的免费教程,使我们学习起来更 ...

  7. 【C++深入探索】Copy-and-swap idiom详解和实现安全自我赋值

    任何管理某资源的类比如智能指针需要遵循一个规则(The Rule of Three): 如果你需要显式地声明一下三者中的一个:析构函数.拷贝构造函数或者是拷贝赋值操作符,那么你需要显式的声明所有这三者 ...

  8. 《Effective Modern C++》Item 6: Use the explicitly typed initializer idiom when auto deduces undesired

    引子 之前Item 5介绍了auto关键字的优点,当然在Item 2我们看到了auto的一些不足,比如由于auto也是在用模板类型做推导,所以某些情况下会丢掉CV修饰符.但这个缺点似乎没有那么有说服力 ...

  9. 关于Impl idiom

    个人理解 c++中头文件这种东西在项目大到一定规模以后,就是罪恶了. 随便更改头文件里面某个class的private成员/函数,都会导致依赖文件的rebuild,编译时间增长 Impl便是现在能够想 ...

  10. 【C++学习】Effective C++

    本文为Effective C++的学习笔记,第一遍学习有很多不理解的地方,后续需要复习. 0 导读 术语 声明(declaration) 告诉编译器某个东西的名称和类型,但略去细节: 每个函数的声明揭 ...

最新文章

  1. redis中的zset
  2. 毕业论文 | 信号的抽取与插值技术研究(源代码)
  3. wordpress如何设置文章置顶以及区分置顶文章与普通文章
  4. 疯狂的华为MateX2:375万人在线抢,转手一台赚2万
  5. javascript Control flow(控制语句)
  6. 绝佳时机,前所未遇,让艰巨作业全自动化
  7. oracle系列(二)oracle体系结构和用户管理
  8. WINCC AUDIT审计组建教程
  9. html生物代码,方舟生存进化生物代码 手游生物指令大全
  10. Workbook.SaveAs方法
  11. Jquery仿IGoogle实现可拖动窗口(源码)
  12. ViewBinding
  13. 雷军赞赏有加,黑鲨游戏手机2打造“操控之王”
  14. 基础IT技术学习资料300篇,欢迎一键收藏
  15. linux系统安装telnet服务
  16. QQ空间小秘书 V1.13 beta3~~ 天空原创软件
  17. 电脑进共享云盘报错“不允许一个用户使用一个以上用户名与服务器或共享资源的多重连接......”
  18. 用java获取一维数组的平均值_java中一维数组常见运算
  19. 中国计算机发展的历史和现状
  20. 12款国内外企业协作工具推荐

热门文章

  1. 微信小程序实现服务通知 模板消息详解(附源码)
  2. 英语科技论文写作语法积累
  3. 前端高效开发必备的js库梳理,日常使用中会持续更新
  4. 电子邮件注册网站哪个好:四大邮箱客户端的对比
  5. 雷达动目标检测matlab代码,【代码分享】基于最大互信息的运动目标检测[matlab源码]...
  6. java计算机毕业设计房屋租赁系统源码+数据库+系统+lw文档+部署
  7. Word文档快速调整表格列宽度
  8. 让devcpp支持c++11
  9. 粗糙集(Rough Sets)
  10. Spring boot 集成 Kaptcha 实现前后端分离验证码功能