標籤:設計模式 一個 實現 對象 參考 scott return red 釋放
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_INCLUDEDclass 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_INCLUDEDclass 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慣用法解析