Pimpl(Pointer to Implementation)惯用法 通過將類的私有數據成員移至一個前向聲明的實現結構體中,僅在頭文件保留一個指向該結構體的指針,從而切斷類使用者對內部依賴頭文件的編譯依賴,顯著縮短編譯時間。C++11 引入 std::unique_ptr 後,Pimpl 的資源管理更安全,但也帶來了特殊成員函數生成的新挑戰——本條款正是針對此問題的最佳實踐總結。
// widget.h
class Widget {
public:
Widget();
~Widget(); // 必須聲明,否則編譯器生成的析構無法刪除前向聲明的 Impl
private:
struct Impl; // 前向聲明
Impl* pImpl; // 原始指針
};
// widget.cpp
#include "widget.h"
#include "gadget.h"
#include <string>
#include <vector>
struct Widget::Impl {
std::string name;
std::vector<double> data;
Gadget g1, g2, g3;
};
Widget::Widget() : pImpl(new Impl) {}
Widget::~Widget() { delete pImpl; } // 手動釋放
std::unique_ptr + 自動管理)// widget.h
#include <memory>
class Widget {
public:
Widget();
~Widget(); // 必須聲明(見下文)
Widget(Widget&&) = default; // 移動構造
Widget& operator=(Widget&&) = default; // 移動賦值
// 拷貝操作需自定義深拷貝(見後文)
private:
struct Impl;
std::unique_ptr<Impl> pImpl; // 智能指針管理生命週期
};
std::unique_ptr 的默認刪除器要求完整類型std::unique_ptr 的默認刪除器在銷毀時會對存儲的指針執行 delete,C++11 要求被刪除的類型必須是完整類型(即編譯器已見過其定義)。若在頭文件中不聲明析構函數,編譯器會在使用者代碼中隱式生成內聯析構函數,此時 Impl 仍是不完整類型 → 觸發 static_assert 失敗 → 編譯報錯。
// widget.h
class Widget {
public:
Widget();
~Widget(); // 僅聲明
...
};
// widget.cpp
#include "widget.h"
#include "gadget.h"
#include <string>
#include <vector>
struct Widget::Impl { ... };
Widget::Widget() : pImpl(std::make_unique<Impl>()) {}
Widget::~Widget() = default; // 此處 Impl 已完整,default 正確調用 unique_ptr 析構
同理,聲明移動構造/賦值但在實現文件 = default,確保編譯器生成的代碼能見到完整的 Impl。
// widget.h Widget(Widget&&); Widget& operator=(Widget&&); // widget.cpp Widget::Widget(Widget&&) = default; Widget& Widget::operator=(Widget&&) = default;
unique_ptr 不可拷貝,編譯器不會生成拷貝操作。若業務需要拷貝語義,必須手寫深拷貝:
// widget.h
Widget(const Widget&);
Widget& operator=(const Widget&);
// widget.cpp
Widget::Widget(const Widget& rhs)
: pImpl(std::make_unique<Impl>(*rhs.pImpl)) {} // 利用 Impl 編譯器生成的拷貝構造
Widget& Widget::operator=(const Widget& rhs) {
*pImpl = *rhs.pImpl; // 利用 Impl 編譯器生成的拷貝賦值
return *this;
}
std::shared_ptr 不存在此問題shared_ptr 的刪除器類型不屬於智能指針類型一部分(類型擦除),控制塊在運行時綁定刪除器,因此編譯器生成的特殊成員函數不要求指向類型完整。使用 shared_ptr 實現 Pimpl 時,無需在頭文件聲明析構/移動操作,代碼更簡潔,但會增加控制塊開銷和原子引用計數開銷。獨占所有權場景仍首選 unique_ptr。
std::unique_ptr 管理實現對象生命週期。= default,也要確保編譯器在實現文件見到完整的 Impl 定義。std::unique_ptr;改用 std::shared_ptr 則無需額外聲明/定義,但語義與開銷不同。最佳實踐模板
// widget.h class Widget { public: Widget(); ~Widget(); Widget(Widget&&); Widget& operator=(Widget&&); Widget(const Widget&); Widget& operator=(const Widget&); private: struct Impl; std::unique_ptr<Impl> pImpl; }; // widget.cpp #include "widget.h" #include "gadget.h" #include <string> #include <vector> struct Widget::Impl { std::string name; std::vector<double> data; Gadget g1, g2, g3; }; Widget::Widget() : pImpl(std::make_unique<Impl>()) {} Widget::~Widget() = default; Widget::Widget(Widget&&) = default; Widget& Widget::operator=(Widget&&) = default; Widget::Widget(const Widget& rhs) : pImpl(std::make_unique<Impl>(*rhs.pImpl)) {} Widget& Widget::operator=(const Widget& rhs) { *pImpl = *rhs.pImpl; return *this; }