デザインパターン PImpl(Implementationへのポインタ)、別名 Opaque Pointer は、C++のクラスのインターフェースと実装を分離する手段として登場しました。これは、バイナリーインターフェース(ABI)の互換性を保証し、コンパイル時間を短縮し、クラスのユーザーから実装の詳細を隠すために特に重要です。
問題の歴史。
多くのC++プロジェクトでは、公開インターフェースを変更せず、これらのクラスのクライアントを再コンパイルせずにクラスの実装を修正する必要があります。問題は、ヘッダーファイルの変更がすべての依存モジュールの再コンパイルを要求するため、特に大規模なコードベースでは非常にコストがかかることです。PImplは再コンパイルを最小限に抑え、より良いカプセル化を提供します。
問題。
プライベートメンバーを持つクラスをヘッダーファイルで定義する標準的な方法では、コンパイル時にすべてのメンバーを知る必要があります。これらを拡張または変更すると、このヘッダーを含むすべてのファイルを再コンパイルする必要があります。さらに、これはクライアントの実装/構造の詳細を明らかにし、セキュリティやアーキテクチャの整合性に悪影響を及ぼす可能性があります。
解決策。
PImplは、cppで定義された前方宣言された構造体(Impl struct/class)へのポインタを使用することで実装を隠します。これにより、インターフェースに影響を与えることなく実装を変更できます。
コードの例:
// Widget.h class Widget { public: Widget(); ~Widget(); void doSomething(); private: struct Impl; Impl* pimpl; // opaque pointer }; // Widget.cpp #include "Widget.h" struct Widget::Impl { int secret; }; Widget::Widget() : pimpl(new Impl{42}) {} // secrecy inside Widget::~Widget() { delete pimpl; } void Widget::doSomething() { pimpl->secret += 1; }
主な特徴:
PImplで生ポインタの代わりにstd::unique_ptrを使用できますか?
はい、現代的で安全なアプローチは、std::unique_ptr(所有権の共有が必要な場合はstd::shared_ptr)を使用することです。これにより、メモリを適切に管理し、生ポインタのために明示的にデストラクタやコピーオペレーターを書く必要がなくなります:
private: std::unique_ptr<Impl> pimpl;
PImplを持つクラスを移動可能にし、コピー不可にすることはできますか?
はい、移動コンストラクタ/オペレーターを提供し、コピーを削除すれば可能です。例えば:
Widget(Widget&&) noexcept = default; Widget& operator=(Widget&&) noexcept = default; Widget(const Widget&) = delete; Widget& operator=(const Widget&) = delete;
PImplを使用することでパフォーマンスにオーバーヘッドが生じますか?
はい、ポインタの間接参照及び追加の動的メモリ割り当てによって(ヒープ割り当て)。パフォーマンスが重要な構造では、これは重大なマイナスとなる可能性があります。
大企業が、シンプルなデータ構造を含むすべてのクラスにPImplを導入した結果、ポインタの間接参照が頻繁に発生し、簡単な操作が大幅に遅くなりました。
利点:
欠点:
長寿命のUIライブラリプロジェクトでは、PImplは頻繁に内部が変更される複雑なウィジェットのみに適用され、サードパーティクライアント向けのABIを安定した状態に保ちました。
利点:
欠点: