PImpl
"Pointer to implementation" または "pImpl" は、クラスの実装詳細をオブジェクト表現から切り離し、それらを別のクラスに配置して不透明なポインタ経由でアクセスすることで実現する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++ライブラリインターフェースを構築し、コンパイル時の依存関係を減らすために使用されます。
目次 |
[編集] 説明
クラスのprivateデータメンバーはオブジェクト表現に参加し、サイズとレイアウトに影響を与えます。また、クラスのprivateメンバー関数はオーバーロード解決(メンバーアクセスチェックの前に行われる)に参加するため、これらの実装詳細が変更されると、そのクラスのすべてのユーザーが再コンパイルを必要とします。
pImplはこのコンパイル依存関係を取り除きます。実装の変更は再コンパイルを引き起こしません。その結果、ライブラリがABIでpImplを使用している場合、新しいバージョンのライブラリは、古いバージョンとのABI互換性を保ちながら実装を変更できます。
[編集] トレードオフ
pImplイディオムの代替案は次のとおりです。
- インライン実装: privateメンバーとpublicメンバーが同じクラスのメンバーである場合。
- 純粋抽象クラス(OOPファクトリ): ユーザーは軽量な抽象基底クラスへのunique_ptrを取得し、実装詳細は仮想メンバー関数をオーバーライドする派生クラスにあります。
[編集] コンパイルファイアウォール
単純なケースでは、pImplとファクトリメソッドの両方が、実装とクラスインターフェースのユーザー間のコンパイル時依存関係を取り除きます。ファクトリメソッドはvtableへの隠れた依存関係を作成するため、仮想メンバー関数の並べ替え、追加、削除はABIを壊します。pImplアプローチには隠れた依存関係はありませんが、実装クラスがクラステンプレートの特殊化である場合、コンパイルファイアウォールの利点は失われます。インターフェースのユーザーは、正しい特殊化をインスタンス化するためにテンプレート定義全体を観察する必要があります。この場合の一般的な設計アプローチは、パラメータ化を回避するように実装をリファクタリングすることです。これは、C++ Core Guidelinesの別のユースケースです。
例えば、以下のクラステンプレートは、そのprivateメンバーや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では、privateメンバー関数への各呼び出しはポインタを介して間接化されます。privateメンバーがpublicメンバーにアクセスするたびに、別のポインタを介して間接化されます。これらの間接化は両方とも翻訳単位の境界を越えるため、リンク時最適化によってのみ最適化できます。OOファクトリは、publicデータと実装詳細の両方にアクセスするために翻訳単位を越える間接化を必要とし、仮想ディスパッチのため、リンク時オプティマイザの機会はさらに少なくなります。
- 空間オーバーヘッド: pImplはpublicコンポーネントに1つのポインタを追加し、privateメンバーがpublicメンバーへのアクセスを必要とする場合、もう1つのポインタが実装コンポーネントに追加されるか、それを必要とするprivateメンバーへの各呼び出しのパラメータとして渡されます。ステートフルカスタムアロケータがサポートされている場合、アロケータインスタンスも格納する必要があります。
- ライフタイム管理オーバーヘッド: pImpl(およびOOファクトリ)は、実装オブジェクトをヒープに配置します。これにより、構築と破棄時にかなりの実行時オーバーヘッドが発生します。pImpl(OOファクトリではない)のアロケーションサイズはコンパイル時に既知であるため、カスタムアロケータによって部分的に相殺される可能性があります。
一方で、pImplクラスは移動フレンドリーです。大規模なクラスを移動可能なpImplとしてリファクタリングすると、そのようなオブジェクトを保持するコンテナを操作するアルゴリズムのパフォーマンスが向上する可能性があります。ただし、移動可能なpImplには追加の実行時オーバーヘッドがあります。移動済みオブジェクトで許可され、プライベート実装へのアクセスを必要とするすべてのpublicメンバー関数は、nullポインタチェックを伴います。
| このセクションは未完成です 理由: マイクロベンチマーク?) |
[編集] メンテナンスオーバーヘッド
pImplを使用するには専用の翻訳単位が必要であり(ヘッダオンリーライブラリはpImplを使用できません)、追加のクラス、一連の転送関数、そしてアロケータを使用する場合は、アロケータ使用の実装詳細をpublicインターフェースに公開します。
仮想メンバーはpImplのインターフェースコンポーネントの一部であるため、pImplのモックはインターフェースコンポーネント単独のモックを意味します。テスト可能なpImplは、通常、利用可能なインターフェースを介して完全なテストカバレッジを可能にするように設計されています。
[編集] 実装
インターフェース型のオブジェクトが実装型のオブジェクトのライフタイムを制御するため、実装へのポインタは通常std::unique_ptrです。
std::unique_ptrは、デリータがインスタンス化されるあらゆるコンテキストで指される型が完全な型であることを要求するため、特殊メンバー関数はユーザーが宣言し、実装ファイル内でインラインではなく定義する必要があります。ここで実装クラスは完全です。
constメンバー関数が非constメンバーポインタを介して関数を呼び出すと、実装関数の非constオーバーロードが呼び出されるため、ポインタはstd::experimental::propagate_constまたは同等のものにラップする必要があります。
すべてのprivateデータメンバーとすべてのprivate非仮想メンバー関数は実装クラスに配置されます。すべてのpublic、protected、および仮想メンバーはインターフェースクラスに残ります(代替案の議論についてはGOTW #100を参照)。
privateメンバーのいずれかがpublicまたはprotectedメンバーへのアクセスを必要とする場合、インターフェースへの参照またはポインタをprivate関数にパラメータとして渡すことができます。あるいは、バックリファレンスを実装クラスの一部として維持することもできます。
実装オブジェクトの割り当てに非デフォルトアロケータをサポートする意図がある場合、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; // 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
| このセクションは未完成です 理由: もう一つの代替案である「高速PImpl」について説明する。主な違いは、実装のためのメモリが、不透明なC配列であるデータメンバー(PImplクラス定義内)に予約されていることです。一方、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. |