编译防火墙——C++的Pimpl惯用法解析
http://blog.csdn.net/lihao21/article/details/47610309
Pimpl(pointer to implementation, 指向实现的指针)是一种常用的,用来对“类的接口与实现”进行解耦的方法。这个技巧可以避免在头文件中暴露私有细节(见下图1),因此是促进API接口与实现保持完全分离的重要机制。但是Pimpl并不是严格意义上的设计模式(它是受制于C++特定限制的变通方案),这种惯用法可以看作桥接设计模式的一种特例。
图1: Pimpl惯用法,这里的公有类拥有一个私有指针,该指针指向隐藏的实现类
在类中使用Pimpl惯用法,具有如下优点:
- 降低耦合
- 信息隐藏
- 降低编译依赖,提高编译速度
- 接口与实现分离
为了实现Pimpl,我们先来看一种普通的类的设计方法。
假如我们要设计一书籍类Book,Book包含目录属性,并提供打印书籍信息的对外接口,Book设计如下:
class Book
{
public:
void print();
private:
std::string m_Contents;
};
Book的使用者只需要知道print()接口,便可以使用Book类,看起来一切都很美好。
然而,当某一天,发现Book需要增加一标题属性,对Book类的修改如下:
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也因此常被称为“编译期防火墙“ 。
本文的示例代码可以通过以下链接下载: 下载链接
参考资料
- C++ Programming/Idioms: Pointer To Implementation (pImpl)
- The C++ Pimpl
- Compilation Firewalls
- 《Effective C++(第三版)》, Scott Meyers著, 侯捷译
- PIMPL模式
- 《C++ API设计》, Martin Reddy著,
编译防火墙——C++的Pimpl惯用法解析的更多相关文章
- [转]编译防火墙——C++的Pimpl惯用法解析
impl(pointer to implementation, 指向实现的指针)是一种常用的,用来对“类的接口与实现”进行解耦的方法.这个技巧可以避免在头文件中暴露私有细节(见下图1),因此是促进AP ...
- RAII惯用法详解
[1]什么是RAII惯用法? RAII是Resource Acquisition Is Initialization的缩写,意为“资源获取即初始化”. 它是C++之父Bjarne Stroustrup ...
- Java将Excel中科学计数法解析成数字
需要注意的是一般的科学表达式是1.8E12 1.8E-12 而在Excel中的科学表达式是1.8E+12 1.8E-12 我写的科学计数法的正则表达式是(-?\d+\.?\d*)[Ee]{1}[\+- ...
- WPF - 绑定及惯用法(一)
写在前面:这仍然是一些没有经过严格审阅的文字.虽然我的确执行了初稿.复稿以及审阅等一系列用以保证文章质量的方法,但是仍然担心其中是否有错误.希望您能帮助指出,以在下一次我在版本更新时进行修正.所有的错 ...
- Java 实现《编译原理》简单词法分析功能 - 程序解析
Java 实现<编译原理>简单词法分析功能 - 程序解析 简易词法分析功能 要求及功能 (1)读取一个 txt 程序文件(最后的 # 作为结束标志,不可省去) { int a, b; a ...
- 【转】python---方法解析顺序MRO(Method Resolution Order)<以及解决类中super方法>
[转]python---方法解析顺序MRO(Method Resolution Order)<以及解决类中super方法> MRO了解: 对于支持继承的编程语言来说,其方法(属性)可能定义 ...
- App裂变活动多种玩法解析
移动互联网时代,流量为王.在App获取流量的过程中,有资金的砸广告,没资金的铺渠道,但是不管你有钱没钱,社交平台都是必须重点争夺的流量阵地. 毕竟,截至2018年底,微信及WeChat的合并月活跃账户 ...
- 用java实现一个简易编译器1-词法解析入门
本文对应代码下载地址为: http://download.csdn.net/detail/tyler_download/9435103 视频地址: http://v.youku.com/v_show/ ...
- TS 原理详细解读(5)语法2-语法解析
在上一节介绍了语法树的结构,本节则介绍如何解析标记组成语法树. 对应的源码位于 src/compiler/parser.ts. 入口函数 要解析一份源码,输入当然是源码内容(字符串),同时还提供路径( ...
随机推荐
- IDEA的database插件无法链接mysql的解决办法(08001错误)
1.问题 首先先说问题,用navicat链接数据库正常,mysql控制台操作正常,但是用IDEA的数据库插件链接一直报 08001 错误,具体见下图: 错误:Connection to eshop@l ...
- Jmeter性能测试-----参数化方法CSVRead函数
Jmeter里面参数化的方法有很多,大家可以结合自己的项目情况来使用哪种方式来调用测试 数据. 下面我给大家介绍下Jmeter里CSVRead函数来获取参数的方法: 我这里已去到直播间发表评论为例(这 ...
- python实现王者荣耀英图片收集
一个python写的小爬虫项目,爬虫相关的很容易写,关键是怎么找到爬取图片的位置. 图片位置分析 hero_list_url = 'http://pvp.qq.com/web201605/js/her ...
- 简述raid0,raid1,raid5,raid10 的工作原理及特点
RAID 0 支持1块盘到多块盘,容量是所有盘之和 RAID1 只支持2块盘,容量损失一块盘 RAID 5最少三块盘,不管硬盘数量多少,只损失一块容量 RAID 10最少4块盘,必须偶数硬盘,不管硬盘 ...
- ReflectionUtil
import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang ...
- 04.Spring Ioc 容器 - 刷新
基本概念 Spring Ioc 容器被创建之后,接下来就是它的初始化过程了.该过程包含了配置.刷新两个步骤 . 刷新由 Spring 容器自己实现,具体发生在 ConfigurableApplicat ...
- (转)vmware克隆Centos6.4虚拟机网卡无法启动问题
vmware克隆Centos6.4虚拟机网卡无法启动问题 原文:http://blog.51cto.com/oldboy/1363853 快速处理办法: cat /etc/sysconfig/netw ...
- RAID磁盘分区的创建
RAID(Reduntant Arrays of Independent Disks),即独立冗余磁盘阵列. RAID的级别: Raid 0: 条带 Raid 1: 镜像 Raid 2,3,4,5 等 ...
- android 开发-数据存储之共享参数
android提供5中数据存储方式 数据存储之共享参数 内部存储 扩展存储 数据库存储 网络存储 而共享存储提供一种可以让用户存储保存一些持久化键值对在文件中,以供其他应用对这些共享参数进行调用.共 ...
- PHPGGC学习----理论
本文首发于先知:https://xz.aliyun.com/t/5450 PHPGGC 是一款能够自动生成主流框架的序列化测试payload的工具,类似 Java 中的 ysoserial, 当前支持 ...