多くの答えがありました...しかし、これまでのところ正しい実装はありません。人々はそれらを使用する可能性が高いため、例が正しくないことは少し悲しいです...
"Pimpl" イディオムは "Pointer to Implementation" の略で、"Compilation Firewall" とも呼ばれます。それでは、飛び込みましょう。
1. インクルードはいつ必要ですか?
クラスを使用する場合、次の場合にのみ完全な定義が必要です。
- そのサイズが必要です(クラスの属性)
- そのメソッドの1つにアクセスする必要があります
それを参照するか、それへのポインターを持っているだけの場合、参照またはポインターのサイズは参照/指している型に依存しないため、識別子を宣言するだけで済みます (前方宣言)。
例:
#include "a.h"
#include "b.h"
#include "c.h"
#include "d.h"
#include "e.h"
#include "f.h"
struct Foo
{
Foo();
A a;
B* b;
C& c;
static D d;
friend class E;
void bar(F f);
};
上記の例では、どのインクルードが「便利な」インクルードであり、正確性に影響を与えずに削除できますか? 最も驚くべきことは、「ああ」以外のすべてです。
2. Pimpl の実装
したがって、Pimpl の考え方は、ヘッダーを含める必要がないように、実装クラスへのポインターを使用することです。
- したがって、依存関係からクライアントを分離します
- したがって、コンパイルの波及効果を防ぎます
追加の利点: ライブラリの ABI が保持されます。
使いやすさのために、Pimpl イディオムを「スマート ポインター」管理スタイルで使用できます。
// From Ben Voigt's remark
// information at:
// http://en.wikibooks.org/wiki/More_C%2B%2B_Idioms/Checked_delete
template<class T>
inline void checked_delete(T * x)
{
typedef char type_must_be_complete[ sizeof(T)? 1: -1 ];
(void) sizeof(type_must_be_complete);
delete x;
}
template <typename T>
class pimpl
{
public:
pimpl(): m(new T()) {}
pimpl(T* t): m(t) { assert(t && "Null Pointer Unauthorized"); }
pimpl(pimpl const& rhs): m(new T(*rhs.m)) {}
pimpl& operator=(pimpl const& rhs)
{
std::auto_ptr<T> tmp(new T(*rhs.m)); // copy may throw: Strong Guarantee
checked_delete(m);
m = tmp.release();
return *this;
}
~pimpl() { checked_delete(m); }
void swap(pimpl& rhs) { std::swap(m, rhs.m); }
T* operator->() { return m; }
T const* operator->() const { return m; }
T& operator*() { return *m; }
T const& operator*() const { return *m; }
T* get() { return m; }
T const* get() const { return m; }
private:
T* m;
};
template <typename T> class pimpl<T*> {};
template <typename T> class pimpl<T&> {};
template <typename T>
void swap(pimpl<T>& lhs, pimpl<T>& rhs) { lhs.swap(rhs); }
他の人が持っていないものは何ですか?
- これは単純に 3 つのルールに従います: コピー コンストラクター、コピー代入演算子、およびデストラクターを定義します。
- 強力な保証を実装してそうします: 割り当て中にコピーがスローされた場合、オブジェクトは変更されません。のデストラクタは
T
スローすべきではないことに注意してください...しかし、それは非常に一般的な要件です;)
これに基づいて、Pimpl 化されたクラスをいくらか簡単に定義できるようになりました。
class Foo
{
public:
private:
struct Impl;
pimpl<Impl> mImpl;
}; // class Foo
注: コンパイラは、正しいコンストラクタ、コピー代入演算子、またはデストラクタをここで生成できません。これを行うと、Impl
定義へのアクセスが必要になるためです。したがって、pimpl
ヘルパーにもかかわらず、手動でこれら 4 を定義する必要があります。ただし、pimpl ヘルパーのおかげで、コンパイルは失敗し、未定義の動作に引きずり込まれることはありません。
3. さらに先へ
関数の存在は実装の詳細と見なされることが多いことに注意してくださいvirtual
。Pimpl の利点の 1 つは、戦略パターンの力を活用するための適切なフレームワークがあることです。
そのためには、pimpl の「コピー」を変更する必要があります。
// pimpl.h
template <typename T>
pimpl<T>::pimpl(pimpl<T> const& rhs): m(rhs.m->clone()) {}
template <typename T>
pimpl<T>& pimpl<T>::operator=(pimpl<T> const& rhs)
{
std::auto_ptr<T> tmp(rhs.m->clone()); // copy may throw: Strong Guarantee
checked_delete(m);
m = tmp.release();
return *this;
}
そして、Foo
好きなものを定義することができます
// foo.h
#include "pimpl.h"
namespace detail { class FooBase; }
class Foo
{
public:
enum Mode {
Easy,
Normal,
Hard,
God
};
Foo(Mode mode);
// Others
private:
pimpl<detail::FooBase> mImpl;
};
// Foo.cpp
#include "foo.h"
#include "detail/fooEasy.h"
#include "detail/fooNormal.h"
#include "detail/fooHard.h"
#include "detail/fooGod.h"
Foo::Foo(Mode m): mImpl(FooFactory::Get(m)) {}
の ABI は、Foo
発生する可能性のあるさまざまな変更にまったく関係がないことに注意してください。
- 仮想メソッドはありません
Foo
- のサイズは
mImpl
、それが何を指していても、単純なポインタのサイズです
したがって、クライアントは、メソッドまたは属性のいずれかを追加する特定のパッチについて心配する必要はなく、メモリ レイアウトなどについて心配する必要もありません。自然に機能します。