Namespaces
Variants

PImpl

From cppreference.net
C++ language
General topics
Flow control
Conditional execution statements
Iteration statements (loops)
Jump statements
Functions
Function declaration
Lambda function expression
inline specifier
Dynamic exception specifications ( until C++17* )
noexcept specifier (C++11)
Exceptions
Namespaces
Types
Specifiers
constexpr (C++11)
consteval (C++20)
constinit (C++20)
Storage duration specifiers
Initialization
Expressions
Alternative representations
Literals
Boolean - Integer - Floating-point
Character - String - nullptr (C++11)
User-defined (C++11)
Utilities
Attributes (C++11)
Types
typedef declaration
Type alias declaration (C++11)
Casts
Memory allocation
Classes
Class-specific function properties
Special member functions
Templates
Miscellaneous

「ポインタ to インプリメンテーション」または「pImpl」は、C++の プログラミング技法 であり、実装の詳細を別個のクラスに配置し、不透明なポインタを介してアクセスすることで、クラスのオブジェクト表現から実装の詳細を除去します:

// --------------------
// インターフェース (widget.h)
struct widget
{
    // 公開メンバ
private:
    struct impl; // 実装クラスの前方宣言
    // 実装例の一つ: 他の設計選択肢とトレードオフについては以下を参照
    std::experimental::propagate_const< // const伝播ポインタラッパー
        std::unique_ptr<                // 所有権一元化不透明ポインタ
            impl>> pImpl;               // 前方宣言された実装クラスへのポインタ
};
// ---------------------------
// 実装 (widget.cpp)
struct widget::impl
{
    // 実装詳細
};

この手法は、安定したABIを持つC++ライブラリインターフェースを構築し、コンパイル時の依存関係を減らすために使用されます。

目次

説明

クラスの非公開データメンバーはそのオブジェクト表現に影響し、サイズとレイアウトに関与するため、またクラスの非公開メンバー関数は オーバーロード解決 (メンバーアクセスチェックの前に行われる)に関与するため、これらの実装詳細に対する変更はクラスのすべてのユーザーの再コンパイルを必要とします。

pImplはこのコンパイル依存性を除去します。実装を変更しても再コンパイルは発生しません。その結果、ライブラリがそのABIでpImplを使用する場合、新しいバージョンのライブラリは実装を変更しながらも、古いバージョンとのABI互換性を維持することができます。

トレードオフ

pImplイディオムの代替案は以下の通りです。

  • inline実装: privateメンバとpublicメンバは同じクラスのメンバです。
  • 純粋抽象クラス (OOPファクトリ): ユーザーは軽量または抽象基底クラスへのユニークポインタを取得し、実装の詳細は仮想メンバ関数をオーバーライドする派生クラスにあります。

コンパイルファイアウォール

単純なケースでは、pImplとファクトリメソッドの両方が、実装とクラスインターフェースの利用者間のコンパイル時依存性を除去します。ファクトリメソッドはvtableへの隠れた依存関係を作り出すため、仮想メンバ関数の並べ替え、追加、削除はABIを破壊します。pImplアプローチには隠れた依存関係はありませんが、実装クラスがクラステンプレートの特殊化である場合、コンパイル防火壁の利点は失われます:インターフェースの利用者は、正しい特殊化をインスタンス化するためにテンプレート定義全体を参照する必要があります。この場合の一般的な設計アプローチは、パラメータ化を回避する方法で実装をリファクタリングすることであり、これはC++ Core Guidelinesの別のユースケースです:

例えば、以下のクラステンプレートは、プライベートメンバーや T push_back 本体で型 T を使用していません:

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

したがって、privateメンバーはそのまま実装に移行でき、 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では、非公開メンバー関数への各呼び出しがポインタを介して間接参照されます。非公開メンバーによって行われる公開メンバーへの各アクセスは、別のポインタを介して間接参照されます。両方の間接参照は翻訳単位の境界を越えるため、リンク時最適化によってのみ最適化除去できます。OOファクトリは、公開データと実装詳細の両方にアクセスするために翻訳単位を越える間接参照を必要とし、仮想ディスパッチのためリンク時オプティマイザによる最適化の機会がさらに少ないことに注意してください。
  • メモリオーバーヘッド: pImplは公開コンポーネントに1つのポインタを追加し、非公開メンバーが公開メンバーにアクセスする必要がある場合、実装コンポーネントに別のポインタが追加されるか、それを必要とする非公開メンバーへの各呼び出しに対してパラメータとして渡されます。状態を持つカスタムアロケータがサポートされている場合、アロケータインスタンスも保存する必要があります。
  • ライフタイム管理オーバーヘッド: pImpl(およびOOファクトリ)は実装オブジェクトをヒープに配置するため、構築時と破棄時にかなりの実行時オーバーヘッドが生じます。これはカスタムアロケータによって部分的に相殺される可能性があります。pImpl(OOファクトリではない)の割り当てサイズはコンパイル時に既知であるためです。

一方で、pImplクラスはムーブ操作に適しています。大規模なクラスをムーブ可能なpImplとしてリファクタリングすると、そのようなオブジェクトを保持するコンテナを操作するアルゴリズムのパフォーマンスが向上する可能性があります。ただし、ムーブ可能なpImplには追加の実行時オーバーヘッドがあります:ムーブ後オブジェクトで許可され、プライベート実装へのアクセスを必要とするすべてのpublicメンバ関数は、nullポインタチェックを発生させます。

メンテナンスオーバーヘッド

pImplの使用には専用の翻訳単位が必要です(ヘッダーのみのライブラリはpImplを使用できません)、追加のクラス、一連の転送関数を導入し、アロケータが使用されている場合、公開インターフェースでアロケータ使用の実装詳細を公開します。

virtualメンバはpImplのインターフェースコンポーネントの一部であるため、pImplのモック作成はインターフェースコンポーネントのみのモック作成を意味します。テスト可能なpImplは通常、利用可能なインターフェースを通じて完全なテストカバレッジを可能にするように設計されています。

実装

インターフェース型のオブジェクトが実装型のオブジェクトの寿命を制御するため、実装へのポインタは通常 std::unique_ptr となります。

std::unique_ptr は、デリーターがインスタンス化される任意のコンテキストで指し示される型が完全型であることを要求するため、特殊メンバ関数はユーザー宣言され、実装クラスが完全である実装ファイル内でアウトオブラインで定義されなければなりません。

constメンバー関数が非constメンバーポインターを通じて関数を呼び出す場合、実装関数の非constオーバーロードが呼び出されるため、ポインターは std::experimental::propagate_const または同等のものでラップする必要があります。

すべてのプライベートデータメンバーとすべてのプライベート非仮想メンバー関数は実装クラスに配置されます。すべてのpublic、protected、および仮想メンバーはインターフェースクラスに残ります(代替案の議論については GOTW #100 を参照してください)。

プライベートメンバーのいずれかがパブリックまたはプロテクテッドメンバーにアクセスする必要がある場合、インターフェースへの参照またはポインタがプライベート関数にパラメータとして渡されることがあります。あるいは、バックリファレンスが実装クラスの一部として維持されることもあります。

実装オブジェクトの割り当てにデフォルト以外のアロケータをサポートする意図がある場合、通常のアロケータ認識パターンのいずれかを利用できます。これには、テンプレートパラメータのデフォルトを std::allocator に設定することや、 std::pmr::memory_resource* 型のコンストラクタ引数などが含まれます。

注記

const伝播を伴うpImpl、バックリファレンスをパラメータとして渡す、アロケータ対応なし、実行時チェックなしでムーブ対応の実装例を示します:

// ----------------------
// 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; // 実装に転送される公開API
    void draw();
    bool shown() const { return true; } // 実装が呼び出す必要がある公開API
    widget(); // デフォルトコンストラクタも実装ファイルで定義する必要がある
              // 注: デフォルト構築されたオブジェクトでのdraw()呼び出しは未定義動作
    explicit widget(int);
    ~widget(); // 実装ファイルで定義(implが完全型である場所)
    widget(widget&&); // 実装ファイルで定義
                      // 注: ムーブ後のオブジェクトでのdraw()呼び出しは未定義動作
    widget(const widget&) = delete;
    widget& operator=(widget&&); // 実装ファイルで定義
    widget& operator=(const widget&) = delete;
};
// ---------------------------
// implementation (widget.cpp)
// #include "widget.hpp"
class widget::impl
{
    int n; // プライベートデータ
public:
    void draw(const widget& w) const
    {
        if (w.shown()) // この公開メンバ関数の呼び出しにはバックリファレンスが必要
            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. The Pimpl Pattern - what you should know.
日本語訳:
1. GotW #28 : 高速Pimplイディオム
2. GotW #100 : コンパイルファイアウォール
3. Pimplパターン - 知っておくべきこと
注意点: - HTMLタグと属性はそのまま保持 - C++専門用語(GotW, Pimpl, Idiom, Compilation Firewalls)は翻訳せず、技術用語として適切な日本語訳を採用 - 番号と記号はそのまま保持 - リンクテキストは適切に翻訳