1. 程式人生 > >C++ 學習筆記(17) pimpl

C++ 學習筆記(17) pimpl

pimpl

物件內含一個指標,指標指向另一個物件,另一個物件常常含有真正的資料。典型例子就是智慧指標。

基本形式:

class other {} ;

class Object {
public:
	// ...
private:
	other *ptr;
} ;

其中, ptr 常是 std::shared_ptr<other> ptr, 遵循 RAII 思想;

優點:

(1)邏輯與實現分離,隱藏真正的資料、

2018.9.17 更新:參考劉未鵬前輩的部落格 舊話重提:pImpl慣用手法的背後,關於pimpl 編譯期防火牆的理解

例子:自定義資料管理類,真正的資料在客戶端內部

基本的使用者類 

// 使用者
class User {
private:
	std::string name;
	std::string address;
	// ......
} ;

真正的資料管理類

// 資料庫, 儲存 User 資訊
class dataBase {
private:
	std::vector<User> userHolder;
public:
        // 只需要負責資料操作
	void addUser(User &One) {
		userHolder.emplace_back(std::move(One));
	}
} ;

Handle 類,負責邏輯控制

// 客戶端, 間接對資料庫操作
// 可在操縱資料之前包攬一些邏輯操作, 準備工作, 清理操作
class dataBasePimpl {
private:
	dataBase *ptr;
public:
	dataBasePimpl(dataBase *_ptr) : ptr(_ptr)
	 {}
	void addUser(User &One) {

		// 進行一些 if 邏輯判斷, 驗證操作合法性等

		if(ptr == nullptr) return;

		// 前期準備工作, 打日誌, 加執行緒鎖

		ptr->addUser(One);  // 真正的資料實現

		// 後期清理工作, 解執行緒鎖
	}
	~dataBasePimpl() {
            // 負責控制 dataBase 的銷燬
		if(ptr not_eq nullptr)
			delete ptr, ptr = nullptr;
	}
} ;

以上檔案分別位於三個檔案中。

如果突然要為 addUser 函式加上執行緒鎖, 就只需要改變 dataBasePimpl 的內容,而不需要改動實現部分 dataBase,  dataBase 所在檔案不需要重新編譯。pimpl 手法降低了檔案之間的編譯相依性。

(2)指標擁有訪問權,可以和 swap 結合,提高效率

考慮一種情況,要交換兩個大型物件,如果採用標準 std::swap,不但交換的資料多,而且大物件儲存在多個頁表中,頁表命中率低。

這時候,可以選擇 pimpl 手法,通過一個指標指向大型物件,交換時只需要交換指標即可。

// 大型物件
class largeObject {
private:
	static constexpr size_t initSize = 1e5;
	std::array<std::string, initSize> userName;
} ;

// pimpl 類
class pimpl {
private:
	shared_ptr<largeObject> ptr;
} ;

如果有兩個大型物件 A, B 需要交換,只需要 std::swap 交換 pimpl 即可。swap 的原始碼大致如下:(摘自 《effective C++》)

namespace std {
	template<typename T>
	void swap(T &a, T &b) {
		T temp(a);
		a = b;
		b = temp;
	}
}

標準庫版 swap 直接交換物件,的確可以完成 pimpl 指標的交換,但是其中共呼叫一次拷貝構造,兩次賦值操作符,一次解構函式,而實際的工作只需要:

std::swap(A.ptr, B.ptr);

不呼叫額外的函式,但鑑於封裝的原因,不能直接訪問 private 成員 ptr。

解決辦法:

(1)返回 handles 引用

class pimpl {
	using ptrType = std::shared_ptr<largeObject>;
private:
	ptrType ptr;
public:
	ptrType& get() {
		return ptr;
	}
} ;

最後只需要 std::swap(A.get(), B.get()); 從介面方面來說,客戶容易呼叫這個非 const 而且返回內部 reference 的函式,容易破壞封裝。

(2)過載 swap 函式(摘自《effective C++》)

class pimpl {
	using ptrType = std::shared_ptr<largeObject>;
private:
	ptrType ptr;
public:
	pimpl() : ptr(std::make_shared<largeObject>()) {}

	// 宣告內部 swap 函式, 在內部交換 private 資料
	void swap(pimpl &other) {
		using std::swap;
		swap(ptr, other.ptr);
	}
} ;

namespace std {
	// 全特化 std::swap 
	template<>
	void swap<pimpl>(pimpl &A, pimpl &B) {
		A.swap(B);
	}
}

按照以上兩個方法,可以避免交換兩個大型物件帶來的消耗,很巧妙。感謝 pimpl 手法。