PImpl
「ポインタ 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ポインタチェックを発生させます。
|
このセクションは不完全です
理由: Microbenchmark?) |
メンテナンスオーバーヘッド
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* 型のコンストラクタ引数などが含まれます。
注記
|
このセクションは不完全です
理由: value-semantic polymorphism との関連性に注記が必要 |
例
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
|
この節は不完全です
理由: 別の代替案「高速PImpl」について説明する必要があります。主な違いは、実装のためのメモリが(PImplクラス定義内の)不透明なC配列であるデータメンバー内に確保され、cppファイルでそのメモリが(
reinterpret_cast
またはプレースメント
new
を通じて)実装構造体にマッピングされる点です。このアプローチには独自の利点と欠点があり、特に明らかな
利点
は、PImplクラスの
設計時
に十分なメモリが初期確保されていれば、追加のアロケーションが不要なことです。(一方、
欠点
としては、ムーブ操作の親和性が低下することが挙げられます。)
|
外部リンク
| 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パターン - 知っておくべきこと |