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 手法。