名稱空間
變體
操作

PImpl

來自 cppreference.com
< cpp‎ | 語言
 
 
C++ 語言
 

"PImpl"(Pointer to implementation,指向實現的指標)是一種 C++ 程式設計技術,它將類的實現細節從其物件表示中移除,透過不透明指標將它們放置在單獨的類中。

// --------------------
// interface (widget.h)
struct widget
{
    // public members
private:
    struct impl; // forward declaration of the implementation class
    // One implementation example: see below for other design options and trade-offs
    std::experimental::propagate_const< // const-forwarding pointer wrapper
        std::unique_ptr<                // unique-ownership opaque pointer
            impl>> pImpl;               // to the forward-declared implementation class
};
 
// ---------------------------
// implementation (widget.cpp)
struct widget::impl
{
    // implementation details
};

此技術用於構建具有穩定 ABI 的 C++ 庫介面,並減少編譯時依賴。

目錄

[編輯] 解釋

因為類的私有資料成員參與其物件表示,影響大小和佈局,並且因為類的私有成員函式參與過載決議(在成員訪問檢查之前進行),所以對這些實現細節的任何更改都需要重新編譯類的所有使用者。

PImpl 消除了這種編譯依賴;對實現的更改不會導致重新編譯。因此,如果庫在其 ABI 中使用 PImpl,則新版本的庫可以更改實現,同時與舊版本保持 ABI 相容。

[編輯] 權衡

PImpl 慣用語的替代方案是

  • 內聯實現:私有成員和公共成員是同一類的成員。
  • 純抽象類(OOP 工廠):使用者獲取指向輕量級或抽象基類的唯一指標,實現細節位於覆蓋其虛成員函式的派生類中。

[編輯] 編譯防火牆

在簡單情況下,PImpl 和工廠方法都消除了實現與類介面使用者之間的編譯時依賴。工廠方法建立了對虛擬函式表的隱藏依賴,因此重新排序、新增或刪除虛成員函式會破壞 ABI。PImpl 方法沒有隱藏依賴,但是如果實現類是類模板特化,則編譯防火牆的優勢會喪失:介面的使用者必須觀察整個模板定義才能例項化正確的特化。在這種情況下,一種常見的設計方法是以避免參數化的方式重構實現,這是 C++ Core Guidelines 的另一個用例。

例如,以下類模板在其私有成員或 `push_back` 的主體中不使用型別 `T`。

template<class T>
class ptr_vector
{
    std::vector<void*> vp;
public:
    void push_back(T* p)
    {
        vp.push_back(p);
    }
};

因此,私有成員可以原樣轉移到實現中,並且 `push_back` 可以轉發到一個在介面中也不使用 `T` 的實現中。

// ---------------------
// header (ptr_vector.hpp)
#include <memory>
 
class ptr_vector_base
{
    struct impl; // does not depend on T
    std::unique_ptr<impl> pImpl;
protected:
    void push_back_fwd(void*);
    void print() const;
    // ... see implementation section for special member functions
public:
    ptr_vector_base();
    ~ptr_vector_base();
};
 
template<class T>
class ptr_vector : private ptr_vector_base
{
public:
    void push_back(T* p) { push_back_fwd(p); }
    void print() const { ptr_vector_base::print(); }
};
 
// -----------------------
// source (ptr_vector.cpp)
// #include "ptr_vector.hpp"
#include <iostream>
#include <vector>
 
struct ptr_vector_base::impl
{
    std::vector<void*> vp;
 
    void push_back(void* p)
    {
        vp.push_back(p);
    }
 
    void print() const
    {
        for (void const * const p: vp) std::cout << p << '\n';
    }
};
 
void ptr_vector_base::push_back_fwd(void* p) { pImpl->push_back(p); }
ptr_vector_base::ptr_vector_base() : pImpl{std::make_unique<impl>()} {}
ptr_vector_base::~ptr_vector_base() {}
void ptr_vector_base::print() const { pImpl->print(); }
 
// ---------------
// user (main.cpp)
// #include "ptr_vector.hpp"
 
int main()
{
    int x{}, y{}, z{};
    ptr_vector<int> v;
    v.push_back(&x);
    v.push_back(&y);
    v.push_back(&z);
    v.print();
}

可能的輸出

0x7ffd6200a42c
0x7ffd6200a430
0x7ffd6200a434

[編輯] 執行時開銷

  • 訪問開銷:在 PImpl 中,每次呼叫私有成員函式都會透過指標間接訪問。私有成員對公共成員的每次訪問都會透過另一個指標間接訪問。兩次間接訪問都跨越翻譯單元邊界,因此只能透過連結時最佳化來消除。請注意,面向物件工廠需要跨翻譯單元進行間接訪問以訪問公共資料和實現細節,並且由於虛派發,為連結時最佳化提供了更少的機會。
  • 空間開銷:PImpl 為公共元件新增一個指標,如果任何私有成員需要訪問公共成員,則要麼向實現元件新增另一個指標,要麼在每次呼叫需要它的私有成員時將其作為引數傳遞。如果支援有狀態的自定義分配器,則分配器例項也必須儲存。
  • 生命週期管理開銷:PImpl(以及面向物件工廠)將實現物件放在堆上,這在構造和析構時會產生顯著的執行時開銷。這可以透過自定義分配器部分抵消,因為 PImpl(但不是面向物件工廠)的分配大小在編譯時是已知的。

另一方面,PImpl 類是移動友好的;將一個大型類重構為可移動的 PImpl 可能會提高操作包含此類物件的容器的演算法的效能,儘管可移動的 PImpl 具有額外的執行時開銷來源:任何允許在被移動物件上執行並需要訪問私有實現的公共成員函式都會產生空指標檢查。

[編輯] 維護開銷

使用 PImpl 需要一個專用的翻譯單元(僅標頭檔案庫不能使用 PImpl),引入一個額外的類,一組轉發函式,並且,如果使用分配器,則在公共介面中暴露分配器使用的實現細節。

由於虛成員是 PImpl 介面元件的一部分,模擬 PImpl 意味著僅模擬介面元件。可測試的 PImpl 通常設計為允許透過可用介面進行全面的測試覆蓋。

[編輯] 實現

由於介面型別的物件控制實現型別物件的生命週期,指向實現的指標通常是 std::unique_ptr

由於 std::unique_ptr 要求在刪除器被例項化的任何上下文中,所指向的型別是完整型別,因此特殊成員函式必須在實現檔案中,在實現類是完整型別的地方,由使用者宣告和定義為非內聯。

因為當 `const` 成員函式透過非 `const` 成員指標呼叫函式時,會呼叫實現函式的非 `const` 過載,所以該指標必須被包裝在 std::experimental::propagate_const 或等效物中。

所有私有資料成員和所有私有非虛成員函式都放置在實現類中。所有公共、受保護和虛成員保留在介面類中(有關替代方案的討論,請參閱 GOTW #100)。

如果任何私有成員需要訪問公共或受保護成員,則可以將對介面的引用或指標作為引數傳遞給私有函式。或者,反向引用可以作為實現類的一部分進行維護。

如果打算支援非預設分配器來分配實現物件,則可以使用任何常見的分配器感知模式,包括分配器模板引數預設為 std::allocator 和型別為 std::pmr::memory_resource* 的建構函式引數。

[編輯] 注意

[編輯] 示例

演示了一個 PImpl,具有 `const` 傳播、將反向引用作為引數傳遞、不感知分配器,以及無需執行時檢查即可移動

// ----------------------
// interface (widget.hpp)
#include <experimental/propagate_const>
#include <iostream>
#include <memory>
 
class widget
{
    class impl;
    std::experimental::propagate_const<std::unique_ptr<impl>> pImpl;
public:
    void draw() const; // public API that will be forwarded to the implementation
    void draw();
    bool shown() const { return true; } // public API that implementation has to call
 
    widget(); // even the default ctor needs to be defined in the implementation file
              // Note: calling draw() on default constructed object is UB
    explicit widget(int);
    ~widget(); // defined in the implementation file, where impl is a complete type
    widget(widget&&); // defined in the implementation file
                      // Note: calling draw() on moved-from object is UB
    widget(const widget&) = delete;
    widget& operator=(widget&&); // defined in the implementation file
    widget& operator=(const widget&) = delete;
};
 
// ---------------------------
// implementation (widget.cpp)
// #include "widget.hpp"
 
class widget::impl
{
    int n; // private data
public:
    void draw(const widget& w) const
    {
        if (w.shown()) // this call to public member function requires the back-reference 
            std::cout << "drawing a const widget " << n << '\n';
    }
 
    void draw(const widget& w)
    {
        if (w.shown())
            std::cout << "drawing a non-const widget " << n << '\n';
    }
 
    impl(int n) : n(n) {}
};
 
void widget::draw() const { pImpl->draw(*this); }
void widget::draw() { pImpl->draw(*this); }
widget::widget() = default;
widget::widget(int n) : pImpl{std::make_unique<impl>(n)} {}
widget::widget(widget&&) = default;
widget::~widget() = default;
widget& widget::operator=(widget&&) = default;
 
// ---------------
// user (main.cpp)
// #include "widget.hpp"
 
int main()
{
    widget w(7);
    const widget w2(8);
    w.draw();
    w2.draw();
}

輸出

drawing a non-const widget 7
drawing a const widget 8

[編輯] 外部連結

1.  GotW #28 : The Fast Pimpl Idiom。
2.  GotW #100: Compilation Firewalls。
3.  Pimpl 模式 - 你應該知道什麼。