pImpl
「実装へのポインタ」 (pointer to implementation) (pImpl) は、クラスの実装の詳細を、不透明なポインタを通してアクセスされる別のクラスに配置することによってそのオブジェクト表現から除去する、 C++ のプログラミングテクニック[1]です。
// widget.h (インタフェース)
class 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 イディオムの代替には以下のものがあります。
- インライン実装。 プライベートメンバとパブリックメンバが同じクラスのメンバです。
- 純粋仮想クラス (オブジェクト指向のファクトリ)。 ユーザは軽量または抽象基底クラスへの一意なポインタを取得し、実装の詳細はその仮想メンバ関数をオーバーライドする派生クラスに書きます。
コンパイル防火壁
単純なケースでは、 pImpl とファクトリメソッドはどちらもクラスのインタフェースの実装とユーザの間のコンパイル時の依存を断ち切ります。 ファクトリメソッドは vtable に隠れた依存を作るため、仮想メンバ関数の並び替え、追加、削除は ABI を破壊します。 pImpl の手法では隠れた依存はありませんが、実装クラスがクラステンプレートの特殊化の場合、コンパイル防火壁の利点は失われます。 インタフェースのユーザは正しい特殊化を実体化するためにテンプレートの定義全体を見なければなりません。 この場合の一般的な設計手法は、パラメータ化を回避するように実装をリファクタリングすることです。 これは C++ Core Guidelines の T.61 Do not over-parametrize members (メンバをパラメータ化しすぎない) および T.84 Use a non-template core implementation to provide an ABI-stable interface (ABI の安定したインタフェースを提供するために非テンプレートなコア実装を使用する) の別のユースケースです。
例えば、以下のクラステンプレートは、そのプライベートメンバ内または push_back の本体内で、型 T を使用しません。
template<class T>
class ptr_vector {
void **beg, **end, **cap;
public:
void push_back(T* p) {
if (end == cap) reallocate(end - beg + 1);
*end++ = p;
}
};
そのため、プライベートメンバはそのまま実装に移すことができ、 push_back もインタフェース内の T を使用しない実装に移すことができます。
// ヘッダ (ptr_vector.h)
class ptr_vector_base {
struct impl; // T に依存しません。
std::unique_ptr<impl> pImpl;
protected:
void push_back_fwd(void*);
... // 特別なメンバ関数については実装の節を参照してください。
};
template<class T>
class ptr_vector : private ptr_vector_base {
public:
void push_back(T* p) { push_back_fwd(p); }
};
// ソース (ptr_vector.cpp)
struct ptr_vector_base::impl {
void **beg, **end, **cap;
void push_back(void* p) {
if (end == cap) reallocate(end - beg + 1);
*end++ = p;
}
void reallocate(size_t sz) { ... }
};
void ptr_vector_base::push_back_fwd(void* p) { pImpl->push_back(p); }
ptr_vector_base::ptr_vector_base() : pImpl(std::make_unique<impl>()) {}
実行時のオーバーヘッド
- アクセスのオーバーヘッド。 pImpl では、プライベートメンバ関数への呼び出しはすべてポインタを通して間接的に行われます。 プライベートメンバによって行われるパブリックメンバへのアクセスはすべて別のポインタを通して間接的に行われます。 両方向の間接アクセスが翻訳単位の境界線を横切るため、最適化はリンク時最適化によってのみ行うことができます。 ちなみに、オブジェクト指向のファクトリは、パブリックデータと実装の詳細のどちらにアクセスするためにも翻訳単位を越える間接アクセスを要求し、仮想ディスパッチのため、リンク時最適化の機会でさえ、より少なくなることに注意してください。
- 空間のオーバーヘッド。 pImpl はパブリックな部分にポインタをひとつ追加し、プライベートメンバからパブリックメンバへのアクセスを必要とする場合は、ポインタをもうひとつ実装の部分に追加するか、それを必要とするプライベートメンバの呼び出しのたびに引数として渡さなければなりません。 ステートフルなカスタムアロケータをサポートする場合は、アロケータのインスタンスも格納する必要があります。
- 生存期間の管理のオーバーヘッド。 pImpl (オブジェクト指向のファクトリも同様です) は、ヒープに実装オブジェクトを配置します。 これは構築および破棄の際に大きな実行時オーバーヘッドを課します。 これは、 pImpl のための確保サイズはコンパイル時に既知であるため、カスタムアロケータによって多少緩和される場合があります (これはファクトリには適用されません)。
一方、 pImpl クラスはムーブフレンドリーです。 大きなクラスをムーブ可能な pImpl としてリファクタリングすると、そのようなオブジェクトを保持するコンテナを操作するアルゴリズムは性能が向上する場合があります。 しかし、ムーブ可能な pImpl は別の実行時オーバーヘッドの源となります。 ムーブ済みのオブジェクトに対して使用可能なおよびプライベート実装へのアクセスが必要なあらゆるパブリックメンバ関数は、ヌルポインタのチェックが必要です。
| This section is incomplete Reason: Microbenchmark?) |
メンテナンスのオーバーヘッド
pImpl の使用は独立した翻訳単位を要求し (ヘッダオンリーなライブラリは pImpl を使用できません)、追加のクラス、転送関数の集合、およびアロケータを使用する場合はパブリックインタフェースにおけるアロケータ使用の実装の詳細を導入します。
仮想メンバは pImpl のインタフェース部分の一部であるため、 pImpl のモック化はインタフェース部分のみのモック化を暗に意味します。 テスト可能な pImpl は一般的には利用可能なインタフェースを通してフルテストカバレッジが可能なように設計されます。
実装
インタフェース型のオブジェクトが実装型のオブジェクトの生存期間を制御するため、実装へのポインタは通常 std::unique_ptr です。
std::unique_ptr は、デリータが実体化されるあらゆる文脈において、指す先の型が完全型であることを要求するため、特別なメンバ関数は、ユーザ宣言され、かつ、外側で (実装ファイル内の、実装クラスが完全である場所で) 定義されなければなりません。
const メンバ関数が非 const メンバ関数を通して関数を呼ぶと、その実装関数の非 const オーバーロードが呼ばれるため、ポインタは std::experimental::propagate_const またはその同等品にラップする必要があります。
すべてのプライベートデータメンバおよびすべてのプライベート非仮想メンバ関数は実装クラスに配置されます。 すべてのパブリック、プロテクテッド、および仮想関数は、インタフェースクラスに残されます (代替手法の議論については GOTW #100 を参照してください)。
プライベートメンバのいずれかがパブリックまたはプロテクテッドメンバにアクセスする必要がある場合は、インタフェースへの参照またはポインタを、そのプライベートメンバに引数として渡すことができます。 または、逆方向の参照を実装クラスの一部として管理することもできます。
実装オブジェクトの確保のために非デフォルトアロケータのサポートを意図している場合は、通常のアロケータ対応のパターンのいずれか (std::allocator をデフォルトとする Allocator テンプレート引数や、 std::pmr::memory_resource* 型のコンストラクタ引数を含みます) を利用できます。
例
const 伝播付きの、逆方向の参照を引数として渡す、アロケータに対応しない、ムーブ可能だけれども実行時のチェックはしない、 pImpl をデモンストレーションします。
#include <iostream>
#include <memory>
#include <experimental/propagate_const>
// インタフェース (widget.h)
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(int);
~widget(); // 実装ファイル (impl が完全型である場所) で定義されます。
widget(widget&&); // 実装ファイルで定義されます。
// 注: ムーブ後のオブジェクトに対する draw() の呼び出しは未定義です。
widget(const widget&) = delete;
widget& operator=(widget&&); // 実装ファイルで定義されます。
widget& operator=(const widget&) = delete;
};
// 実装 (widget.cpp)
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(int n) : pImpl{std::make_unique<impl>(n)} {}
widget::widget(widget&&) = default;
widget::~widget() = default;
widget& widget::operator=(widget&&) = default;
// ユーザ (main.cpp)
int main()
{
widget w(7);
const widget w2(8);
w.draw();
w2.draw();
}
出力:
drawing a non-const widget 7
drawing a const widget 8
ノート
| This section is incomplete Reason: note connection to value-semantic polymorphism |