References

  • C++ Coding Standard

    這本書的中文版不知道是不是翻譯問題,還是原作就有這種傾向,有些咬文嚼字的很不好懂。

  • Exceptional C++

    這本比上面那本容易理解的多,有提到 PIMPL 實作上需要注意的地方,可惜的是已經絕版了,有趣的是前面那本作者之一也是本書作者。

上面這兩本其實缺乏的是實務面的範例,如果缺乏實務面的說明,PIMPL 對初學者來講就只是炫技罷了,本文多少做了一些補充。

PIMPL (Pointer to Implementation ) 技巧已經出現十幾年了,可是小弟的職業生涯中卻很少看到有人使用,決定來寫篇文章推廣一下。這個手法可以解決/改善 C++ coding 常碰到的 2 大問題:

  1. class 增加 private/protected member,使用此 class 的相關 .cpp(s) 需要重新編譯
  2. 定義衝突與跨平台編譯問題

Q1. class 增加 private/protected member,使用此 class 的相關 .cpp(s) 全部需要重新編譯

假設我們有一個 A.h(class A),並且有 A/B/C/D 4 個 .cpp 引用他,他們的關係如下圖:

假如 A class 增加了 private/protected member,A/B/C/D.cpp 全部都要重新編譯。因為 make 是用檔案的時間戳記來判斷是否要重新編譯,當 make 發現 A.h 比 A/B/C/D.cpp 4個檔案新,就會呼叫 compiler 重新編譯他們,就算你的 C++ compiler 非常聰明,知道這 B/C/D 檔案只能存取 A class public member,make 還是要把 compiler 叫起來檢查。三個檔案也許還好,那五十個,一百個呢?

//a.h
#ifndef A_H
#define A_H #include <memory> class A
{
public:
A();
~A(); void doSomething(); private:
struct Impl;
std::auto_ptr<impl> m_impl;
}; #endif

C++ 有一定程度的人都知道,在尚未取用指標指向的實體前,我們可以用 forward declaration 的技巧告訴 compiler 這是個「指向 class/struct 的指標」,而不用顯露 struct/class 的佈局。

在這裡我們把原本的 private member 封裝到 struct A::Impl 裡,用一個不透明的指標(m_impl) 指向他,auto_ptr 是個 smart pointer(from STL),會在 A class object 銷毀時連帶將資源銷毀還給系統。

a.cpp 如下

//a.cpp
#include <stdio.h>
#include "a.h" struct A::Impl {
int m_count; Impl(); ~Impl(); void doPrivateThing();
}; A::Impl::Impl() : m_count(0) {
} A::Impl::~Impl() {} void A::Impl::doPrivateThing() {
printf("count = %d\n", ++m_count);
} A::A() : m_impl(new Impl) {} A::~A() {} void A::doSomething() {
m_impl->doPrivateThing();
}

上面我們可以看到 A private data/function member 全部被封裝到 struct A::Impl 裡,如此一來無論 private member 如何改變都只會重新編譯 A.cpp,而不會影響 B/C/D.cpp,當然有時候還是會有例外,不過大部分情況下可以替你省下大把的編譯時間,越大的專案越能感受到效果。

Q2. 定義衝突與跨平台編譯問題

如果你運氣很好公司配給你 8 Cores CPU、SSD、32G DDRAM,大概會覺得 PIMPL 是脫褲子放屁。

但定義衝突/跨平台問題不是高速電腦能夠解決的,甚至會讓你的專案卡住。舉個例子,你想在 Windows 上開啟 framework(例如 Qt) 沒有支援的特殊裝置或檔案,你大概會這樣做:

//foo.h
#ifndef FOO_H
#define FOO_H #include <windows.h> class Foo { public:
Foo(); ~Foo(); void doSomething(); private:
HANDLE m_handle; }; #endif

Foo private data member: m_handle 對應到某個特別的檔案或裝置,某天你想把 Foo 移植到Linux,因為 Linux 是用 int 作為 file descriptor,為了與 Windows 的定義區隔,最直接的手段就是用巨集:

//foo.h
#ifndef FOO_H
#define FOO_H #ifdef _WIN32 #include <windows.h> #else
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#endif class Foo { public:
Foo(); ~Foo(); void doSomething(); private: #ifdef _WIN32
HANDLE m_handle;
#else
int m_handle;
#endif }; #endif

這樣做會有什麼問題?

  • windows.h 是個巨大的 header file,有可能會增加引用此 header file 的其他 .cpp(s) 編譯時間,而實際上這些 .cpp 並不需要 windows.h 裡面的資訊。
  • windows.h 會與 framework 衝突,雖然大部分的 framework 極力避免發生這種事,但往往專案成長變大後常常出現這類編譯錯誤(Linux 也可能發生)。
  • 對於 Linux 用戶,Windows 那些 header file 是多餘的,對於 Windows 用戶 Linux header files 是多餘的,沒必要也不該知道這些細節。

Please minimize your header file!

請最小化你的 header file」,這是站長 10 幾年來最重要的工作心得之一。不過有些人會質疑那我不是不能用 inline function?也許是站長孤陋寡聞,站長 10 幾年來還沒碰過非得用 inline 才能解決的問題,而且實務經驗中如 C++ Builder 甚至用 inline 還會發生奇怪的 bug(改成一般的 member function 就好了),事實上很多認為用 inline 會加快的程式都是自由心證缺乏嚴格測試。目前非得放在 header file 大概只有 template,但實務上的很少看過有人自己實作 template,善用 STL 的人就已經不多了。

Yunfei Zu 前辈对 Pimpl 的解释

C++的Pimpl惯用法或者说Pimpl模式,又被称为编译防火墙,是一种在头文件中隐藏实现的方式。Pimpl很古老,可能在标准C++诞生之前就有了这种用法,其间争论也早已尘埃落定,用和不用各有利弊,主要还是看组织内部的规范和项目的需要。最近Team一直同时在两个subsystem下工作,两个subsystem的code base一个用了Pimpl一个没有用,是以在Team中产生了到底要不要用的争论。虽然SA的决定是维持现状,但还是总结下Pimpl的相关知识,以备参考。

Pimpl 没有固定的形式,有的很复杂,如Qt中的private class和D-Pointer的结构。而Team在项目中用到的相对很简单,只是一个智能指针加一个Inner Class, 基本结构如下。

// Foo.h

class Foo {
public:
Foo();
virtual ~Foo();
private:
class Pimpl;
boost::scoped_ptr<Pimpl> _pimpl;
}; // Foo.cpp
class Foo::Pimpl {
public:
// data or functions
} Foo::Foo() : _pimpl(new Pimpl) {} Foo::~Foo() {}

使用这种结构的好处:

  1. 成员变量的修改不会影响类的头文件,避免重新编译所有inclue类头文件的模块
  2. 类的头文件不需要include 成员变量的头文件,减少编译依赖,加快编译速度
  3. 更好的封装类的实现细节

而相应的缺点:

  1. 增加了代码复杂度
  2. 造成代码可读性下降
  3. 由于指针间接调用造成的性能下降

至于要不要使用Pimpl, 要视情况而定。如果你的工程更注重减少依赖,隐藏实现,Pimpl正适合你。相反如果你的工程中的类含有很多虚函数,又会被大量调用,最好考虑下Pimpl会不会带来性能问题。最近就遇到一个例子,一个库和Qt有冲突,在moc class的头文件中inclue这个库的头文件就会有编译问题。最后就是用Pimpl的方法解决了头文件的依赖。

C++ Idioms Pimpl的更多相关文章

  1. More C++ Idioms

    Table of Contents Note: synonyms for each idiom are listed in parentheses. Adapter Template TODO Add ...

  2. [转]编译防火墙——C++的Pimpl惯用法解析

    impl(pointer to implementation, 指向实现的指针)是一种常用的,用来对“类的接口与实现”进行解耦的方法.这个技巧可以避免在头文件中暴露私有细节(见下图1),因此是促进AP ...

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

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

  4. pImpl

    之前看代码,一直对pIml这个用法一知半解,参考这里 的一篇文章后有所收获. 总结一下,pIml的好处如下: 第一,引入更多的头文件降低编译速度.而且这个声明当然写在一个头文件里,而头文件,是不能预编 ...

  5. [021]转 C++ Pimpl机制

    出处:http://www.cnblogs.com/gnuhpc/ 1.简介 这个机制是Private Implementation的缩写,我们常常听到诸如“不要改动你的公有接口”这样的建议,所以我们 ...

  6. 何为 pimpl ?

    前言 你是否总因头文件包含冲突而苦恼? 你是否因头文件包含错乱而苦恼? 你是否因封装暴露了数据而苦恼? 你是否因经常改动实现而导致重新编译而苦恼? 在这里, 这些问题都不是问题, 跟随作者, 揭秘pi ...

  7. PIMPL设计模式的理解和使用

    以下两段不同程序的比较 //file a.h #include "a.h" #include “ b.h” class A{ void Fun(); B  b; } //file: ...

  8. 提高C++编译速度-------pimpl 模式& 桥接模式(转)

    pimpl 模式(Private Implementation),我们常常听到诸如“不要改动你的公有接口”这样的建议,所以我们一般都会修改私有接口,但是这会导致包含该头文件的所有源文件都要重新编译,这 ...

  9. C++ 编译期封装-Pimpl技术

    Pimpl技术——编译期封装 Pimpl 意思为“具体实现的指针”(Pointer to Implementation), 它通过一个私有的成员指针,将指针所指向的类的内部实现数据进行隐藏, 是隐藏实 ...

  10. Item 22: 当使用Pimpl机制时,在实现文件中给出特殊成员函数的实现

    本文翻译自<effective modern C++>,由于水平有限,故无法保证翻译完全正确,欢迎指出错误.谢谢! 博客已经迁移到这里啦 如果你曾经同过久的编译时间斗争过,那么你肯定对Pi ...

随机推荐

  1. C语言输入任意长度数组后,再在该数组中查找特定的值,并且可查找多个相同的值

    C语言输入任意长度数组后,再在该数组中查找特定的值,并且可查找多个相同的值 例:在a[20] = { 99,42,57,74,46,85,32,78,40,33,74,88,65,27,38,69,5 ...

  2. Llinux登录后出现-bash-4.2#,解决办法以及造成这样的原因

    版权声明:原创作品,谢绝转载!否则将追究法律责任. ----- 作者:kirin 1.原因是root在/root下面的几个配置文件丢失,丢失文件如下: 1..bash_profile 2..bashr ...

  3. ABAP 生产订单长文本增强 <销售计划 、物料独立需求 长文本带入 计划订单-生产订单 >

    计划订单长文本带入生产订单 尝试在生产订单保存后 用 creat_text 函数 去创建长文本,发现前台不显示,查看 文本抬头底表 STXL 发现有值 ,用READ 函数 读取 能读. DATA:td ...

  4. Centos8.4自定义离线安装Nginx

    一.简介 Nginx是一个web服务器也可以用来做负载均衡及反向代理使用. 目前使用最多的就是负载均衡,这篇文章主要介绍了centos8 安装 nginx. Nginx是一种开源的高性能HTTP和反向 ...

  5. Guava Preconditions类的各种用法

    公众号「架构成长指南」,专注于生产实践.云原生.分布式系统.大数据技术分享. Guava Preconditions类 提供静态方法列表,用于检查是否使用有效参数值调用方法或构造函数.如果前提条件失败 ...

  6. [清华集训2017] Hello World!

    Hello world! 题目背景 不远的一年前,小 V 还是一名清华集训的选手,坐在机房里为他已如风中残烛的OI 生涯做最后的挣扎.而如今,他已成为了一名光荣的出题人.他感到非常激动,不禁感叹道: ...

  7. 解密Prompt系列21. LLM Agent之再谈RAG的召回信息密度和质量

    话接上文的召回多样性优化,多路索引的召回方案可以提供更多的潜在候选内容.但候选越多,如何对这些内容进行筛选和排序就变得更加重要.这一章我们唠唠召回的信息密度和质量.同样参考经典搜索和推荐框架,这一章对 ...

  8. Socket.D 基于消息的响应式应用层网络协议

    首先根据 Socket.D 官网的副标题,Socket.D 的自我定义是: 基于事件和语义消息流的网络应用协议. 官网定义的特点是: 基于事件,每个消息都可事件路由 所谓语义,通过元信息进行语义描述 ...

  9. Cisco 交换机利用CDP数据自动绘制网络拓扑图[drawio]-实践

    进行网络运维,必须对网络拓扑情况进行详细的掌握,但是网络改动后,更新网络拓扑比较繁琐,维护人员容易懈怠,久而久之,通过人工绘制的网络拓扑很容易与现有网络出现偏差. 现在,可以通过python 丰富的库 ...

  10. Python——Html(内联框架<iframe>)

    <iframe>(内联框架)是 HTML 中的一个标签,用于在当前文档中嵌入另一个文档.它提供了一种将一个 HTML 文档嵌套到另一个文档的方式,允许在一个页面中显示另一个页面的内容.以下 ...