impl(pointer to implementation, 指向实现的指针)是一种常用的,用来对“类的接口与实现”进行解耦的方法。这个技巧可以避免在头文件中暴露私有细节(见下图1),因此是促进API接口与实现保持完全分离的重要机制。但是Pimpl并不是严格意义上的设计模式(它是受制于C++特定限制的变通方案),这种惯用法可以看作桥接设计模式的一种特例。

图1: Pimpl惯用法,这里的公有类拥有一个私有指针,该指针指向隐藏的实现类

在类中使用Pimpl惯用法,具有如下优点:

  • 降低耦合
  • 信息隐藏
  • 降低编译依赖,提高编译速度
  • 接口与实现分离

为了实现Pimpl,我们先来看一种普通的类的设计方法。 
假如我们要设计一书籍类Book,Book包含目录属性,并提供打印书籍信息的对外接口,Book设计如下:

class Book
{
public:
void print(); private:
std::string m_Contents;
};

  

class Book
{
public:
void print(); private:
std::string m_Contents;
std::string m_Title;
};

  虽然使用print()接口仍然可以直接输出书籍的信息,但是Book类的使用者却不得不重新编译所有包含Book类头文件的代码。 
为了隐藏Book类的实现细节,实现接口与实现的真正分离,可以使用Pimpl方法。 
我们依然对Book类提供相同的接口,但Book类中不再包含原有的数据成员,其所有操作都由BookImpl类实现。

/* public.h */
#ifndef PUBLIC_H_INCLUDED
#define PUBLIC_H_INCLUDED class Book
{
public:
Book();
~Book();
void print(); private:
class BookImpl; // Book实现类的前置声明
BookImpl* pimpl;
}; #endif

  

在对外的头文件public.h中,只包含Book类的外部接口,将真正的实现细节被封装到BookImpl类。为了不对外暴露BookImpl类,将其声明为Book类的内嵌类,并声明为private。

BookImpl类的头文件如下。

/* private.h */
#ifndef PRIVATE_H_INCLUDED
#define PRIVATE_H_INCLUDED #include "public.h"
#include <iostream> class Book::BookImpl
{
public:
void print(); private:
std::string m_Contents;
std::string m_Title;
}; #endif

  private.h并不需要提供给Book类的使用者,因此,如果往后需要重新设计书籍类的属性,外界对此一无所知,从而保持接口的不变性,并减少了文件之间的编译依赖关系。

/* book.cpp */
#include "private.h" // 我们需要调用BookImpl类的成员函数,
// 所以要包含BookImpl的定义头文件
#include "public.h" // 我们正在实现Book类,所以要包含Book类
// 的头文件 Book::Book()
{
pimpl = new BookImpl();
} Book::~Book()
{
delete pimpl;
} void Book::print()
{
pimpl->print();
} /* BookImpl类的实现函数 */ void Book::BookImpl::print()
{
std::cout << "print from BookImpl" << std::endl;
}

  使用Book类的接口的方法如下:

/* main.cpp */
#include "public.h" int main()
{
Book book;
book.print(); return 0;
}

  

像Book类这样使用Pimpl的类,往往被称为handle class,BookImpl类作为实现类,被称为implementation class。

为简单实现起见,Book类省略了复制构造函数和复制赋值函数。在实际应用中,一般有两种可选方案解决Book的复制和赋值的语义问题。

(1) 禁止复制类 
如果不打算让用户创建对象的副本,那么可以将对象声明为不可复制的。可以将复制构造函数和复制赋值函数声明为私有的,这样在复制或者赋值时就会产生编译错误。 
以下代码通过声明私有的复制构造函数和复制赋值函数来使得对象不可以复制,不需要修改相关的.cpp文件。

/* public.h */
#ifndef PUBLIC_H_INCLUDED
#define PUBLIC_H_INCLUDED class Book
{
public:
Book();
~Book();
void print(); private:
// 禁止复制类
Book(const Book&);
const Book &operator = (const Book &); class BookImpl; // Book实现类的前置声明
BookImpl* pimpl;
}; #endif

  

(2) 显示定义复制语义 
如果希望用户能够复制采用Pimpl的对象,就应该声明并定义自己的复制构造函数和复制赋值函数。它们可以执行对象的深复制,即创建对象的副本,而非复制指针。

Pimpl惯用法最主要的缺点是,必须为你创建的每个对象分配并释放实现对象,这使对象增加了一个指针,handle class成员函数的每次调用都必须通过implementation class,这会增加一层间接性。在实际中你需要对这些开销进行权衡。 
另外,采用了Pimpl的对象,编译器将不再能够捕获const方法中对成员变量的修改。这是由于成员变量现在存在于独立的对象中,编译器仅检查const方法中的pimpl指针是否发生变化,而不会检查pimpl指向的任何成员。

可以使用下图2来说明Pimpl方法在以上Book类设计的作用:

 
图2: Pimpl作为编译防火墙

由于Pimpl解除了接口与实现之间的耦合关系,从而降低文件间的编译依赖关系,Pimpl也因此常被称为“编译期防火墙“ 。

本文的示例代码可以通过以下链接下载: 下载链接

参考资料

    1. C++ Programming/Idioms: Pointer To Implementation (pImpl)
    2. The C++ Pimpl
    3. Compilation Firewalls
    4. 《Effective C++(第三版)》, Scott Meyers著, 侯捷译
    5. PIMPL模式
    6. 《C++ API设计》, Martin Reddy著,

[转]编译防火墙——C++的Pimpl惯用法解析的更多相关文章

  1. 编译防火墙——C++的Pimpl惯用法解析

    http://blog.csdn.net/lihao21/article/details/47610309 Pimpl(pointer to implementation, 指向实现的指针)是一种常用 ...

  2. RAII惯用法详解

    [1]什么是RAII惯用法? RAII是Resource Acquisition Is Initialization的缩写,意为“资源获取即初始化”. 它是C++之父Bjarne Stroustrup ...

  3. WPF - 绑定及惯用法(一)

    写在前面:这仍然是一些没有经过严格审阅的文字.虽然我的确执行了初稿.复稿以及审阅等一系列用以保证文章质量的方法,但是仍然担心其中是否有错误.希望您能帮助指出,以在下一次我在版本更新时进行修正.所有的错 ...

  4. Java将Excel中科学计数法解析成数字

    需要注意的是一般的科学表达式是1.8E12 1.8E-12 而在Excel中的科学表达式是1.8E+12 1.8E-12 我写的科学计数法的正则表达式是(-?\d+\.?\d*)[Ee]{1}[\+- ...

  5. Java 实现《编译原理》简单词法分析功能 - 程序解析

    Java 实现<编译原理>简单词法分析功能 - 程序解析 简易词法分析功能 要求及功能 (1)读取一个 txt 程序文件(最后的 # 作为结束标志,不可省去) { int a, b; a ...

  6. 【转】python---方法解析顺序MRO(Method Resolution Order)<以及解决类中super方法>

    [转]python---方法解析顺序MRO(Method Resolution Order)<以及解决类中super方法> MRO了解: 对于支持继承的编程语言来说,其方法(属性)可能定义 ...

  7. java惯用法转载

    目录 实现: equals() hashCode() compareTo() clone() 应用: StringBuilder/StringBuffer Random.nextInt(int) It ...

  8. Notepad++ v5.5以上 惯用法教程

    注:本文中为注明为自定义快捷键的,均为notepad++的默认快捷键. 0.  关闭标签页 UltraEdit是双击窗口就可以关闭,Notepad++双击不能关闭,右键只能关闭非当前标签页,那怎么办呢 ...

  9. App裂变活动多种玩法解析

    移动互联网时代,流量为王.在App获取流量的过程中,有资金的砸广告,没资金的铺渠道,但是不管你有钱没钱,社交平台都是必须重点争夺的流量阵地. 毕竟,截至2018年底,微信及WeChat的合并月活跃账户 ...

随机推荐

  1. [BinaryTree] AVL树、红黑树、B/B+树和Trie树的比较

    转自:AVL树.红黑树.B/B+树和Trie树的比较 AVL树 最早的平衡二叉树之一.AVL是一种高度平衡的二叉树,所以通常的结果是,维护这种高度平衡所付出的代价比从中获得的效率收益还大,故而实际的应 ...

  2. 多态在编译器是无法确定引用类型的是哪个子类 可以用 instanceof 在运行期判断

  3. BZOJ 3230 相似子串 | 后缀数组 二分 ST表

    BZOJ 3230 相似子串 题面 题解 首先我们要知道询问的两个子串的位置. 先正常跑一遍后缀数组并求出height数组. 对于每一个后缀suffix(i),考虑以i开头的子串有多少是之前没有出现过 ...

  4. Effective C++ 条款08:别让异常逃离析构函数

    1.别让异常逃离析构函数的原因 <Effective C++>第三版中条款08建议不要在析构函数中抛出异常,原因是C++异常机制不能同时处理两个或两个以上的异常.多个异常同时存在的情况下, ...

  5. java多线程 -- 同步鎖

    为了解决多线程安全问题在 Java 5.0 之前,协调共享对象的访问时可以使用的机制只有 synchronized 和 volatile .Java 5.0 后增加了一些新的机制,但并不是一种替代内置 ...

  6. Unhandled rejection RangeError: Maximum call stack size exceededill install loadIdealTree

    npm安装时候报这个错误,解决方法如下: 删除项目中的 package-lock.json 文件 和 node_modules 文件夹,然后再尝试 npm install 详细参考

  7. 团体程序设计天梯赛 L1-049. 天梯赛座位分配(测试数据+不同方法)

    Data: /*33 2 1#11 4 7 10 13 16 19 22 25 2831 33 35 37 39 41 43 45 47 4951 53 55 57 59 61 63 65 67 69 ...

  8. Java入门:一些初学者需要掌握的基础算法程序——二分查找

    本例演示如何通过二分算法查找一个链表中的指定元素. import java.util.Scanner; class BinarySearchExample { public static void m ...

  9. 并发批量管理500台以上服务器脚本分享(shell版)

    转载于运维笔记 Categories:Shell 也许很多人认为shell不能并发任务,其实可通过其它一些方式来实现.下面的脚本是我批量快速管理500+服务器脚本,阅读该脚本前建议先看<自动执行 ...

  10. Ansible7:Playbook常用模块

    目录 template set_fact pause wait_for assemble add_host group_by debug fail playbook的模块与在ansible命令行下使用 ...