![](https://habrastorage.org/files/ecb/cc1/5b2/ecbcc15b2ab1449c81397c079133c290.png)
Pimpl(実装へのポインター)は、C ++で一般的な便利なイディオムです。 このイディオムにはいくつかの肯定的な側面がありますが、この記事ではコンパイル時間の依存関係を減らす手段としてのみ考えています。 イディオム自体の詳細については、たとえばhere 、 here 、 hereを参照してください 。 この記事では、Pimplを使用する際に使用するスマートポインターと、それが必要な理由に焦点を当てます。
Pimplを実装するためのさまざまなオプションを考えてみましょう。
ベアポインター
多くの人がおそらく見ている最も簡単な方法は、ベアポインターを使用することです。
使用例:
// widget.h class Widget { public: Widget(); ~Widget(); //... private: struct Impl; Impl* d_; }; // widget.cpp struct Widget::Impl { /*...*/ }; Widget::Widget(): d_(new Impl) {} Widget::~Widget() { delete d_; }
長所:
- 追加のエンティティは必要ありません
短所:
- ポインターを明示的に削除する必要がある(誰にもわからないメモリリークの可能性)
- 例外に関しては安全ではありません(Implの作成後にコンストラクターで例外が発生すると、メモリリークが発生します)-一般に、これがスマートポインターを使用する必要がある主な理由です。
std :: auto_ptrを使用する
auto_ptrはすでに禁止されているため、使用しないでください。 ただし、Pimplに関連する問題と同様に、ベアポインターに対する利点を注意することが重要です。
使用例:
// widget.h class Widget { // ... struct Impl; std::auto_ptr<Impl> d_; }; // widget.cpp struct Widget::Impl { /*...*/ }; Widget::Widget(): d_(new Impl) {} Widget::~Widget() {}
auto_ptrは、標準ライブラリの他のスマートポインターと同様に、ポインターの有効期間の管理を担当します。 RAIIのイディオムを使用して、auto_ptrを使用すると、例外に関してPimplで安全に作業できます。例外が発生すると、デストラクタが呼び出され、メモリが解放されるためです。
メモリの自動解放にもかかわらず、Pimplを使用する場合、auto_ptrには非常に危険なプロパティがあります。 驚くほど多くのこのコードが実行されると、警告なしにメモリリークが発生します。
// widget.h class Widget { public: Widget(); //... private: struct Impl; std::auto_ptr<Impl> d_; }; // widget.cpp struct Widget::Impl { /*...*/ }; Widget::Widget(): d_(new Impl) {}
これは、auto_ptrが不完全なクラスを削除するためです。 この問題の詳細については、 こちらをご覧ください 。 この問題はauto_ptrだけに当てはまるものではないため、この問題に精通することを強くお勧めします。 この状況での問題の簡単な解決策は、デストラクタを明示的に宣言および定義することです。
長所:
- 例外に対して安全
短所:
- 禁じられた
- 不完全なクラスを削除するとメモリリークが発生する可能性があります
std :: unique_ptrを使用する
C ++ 11では、移動セマンティクスが導入され、auto_ptrを、予想されるunique_ptr動作のスマートポインターに置き換えることができました。
使用例:
// widget.h class Widget { // ... struct Impl; std::unique_ptr<Impl> d_; }; // widget.cpp struct Widget::Impl { /*...*/ }; Widget::Widget(): d_(std::make_unique<Impl>()) {} Widget::~Widget() {}
unique_ptrは、コンパイル段階で完全性をチェックするときに不完全なクラスを削除する問題を解決します。 不完全なクラスのサイレント削除は失敗します。
ただし、タスクを解決するために、unique_ptrには、通常のポインターのセマンティクスがあるという欠点があります。 例を考えてみましょう:
// widget.h class Widget { public: // ... void foo() const; // <- private: struct Impl; std::unique_ptr<Impl> d_; }; // widget.cpp struct Widget::Impl { int i = 0; }; Widget::Widget(): d_(std::make_unique<Impl>()) {} Widget::~Widget() {} void Widget::foo() const { d_->i = 42; // <- }
ほとんどの場合、このようなコードをコンパイルすることは望ましくありません。
Pimplのイディオムではポインターが使用されるという事実にもかかわらず、ポインターが指すデータには元のクラスに属するというセマンティクスがあります。 論理的不変性の観点から、定数メソッドのImplデータを含むすべてのデータは定数でなければなりません。
長所:
- メモリリーク保護
短所:
- 論理的不変性
propagate_constでの std :: unique_ptrの使用
実験的ライブラリには、propagate_constポインタのラッパーがあり、これにより論理的な不変性を修正できます。
使用例:
// widget.h class Widget { // ... struct Impl; std::experimental::propagate_const<std::unique_ptr<Impl>> d_; }; // widget.cpp struct Widget::Impl { /*...*/ }; Widget::Widget(): d_(std::make_unique<Impl>()) {} Widget::~Widget() {}
これで、前の例のコードによってコンパイルエラーが発生します。
これは問題の完全な解決策に近いようですが、別の小さな点があります。
コンストラクターを作成するときは、常に明示的にImplを作成する必要があります。 ほとんどの場合、実行時にクラスに最初にアクセスしたときにエラーが表示されるため、これは大きな問題ではないようです。
長所:
- 論理的不変性の遵守
短所:
- コンストラクターでImplを作成することを忘れる能力
- propagate_constはまだ標準の一部ではありません
PimplPtrを使用する
上記のすべての長所と短所を考えると、完全なソリューションを実現するには、次の要件を満たすスマートポインターを提供する必要があります。
- 例外に関する安全性
- 部分クラス削除保護
- 論理的不変性の遵守
- Implが作成した保護
最初の2つのポイントは、unique_ptrを使用して実装できます。
template<class T> class PimplPtr { public: using ElementType = typename std::unique_ptr<T>::element_type; // ... private: std::unique_ptr<T> p_; // <- };
3番目の項目はpropagate_constを使用して実装できますが、まだ標準になっていないため、ポインタアクセスメソッドを自分で簡単に実装できます。
const ElementType* get() const noexcept { return p_.get(); } const ElementType* operator->() const noexcept { return get(); } const ElementType& operator*() const noexcept { return *get(); } explicit operator const ElementType*() const noexcept { return get(); } ElementType* get() noexcept { return p_.get(); } ElementType* operator->() noexcept { return get(); } ElementType& operator*() noexcept { return *get(); } explicit operator ElementType*() noexcept { return get(); }
4番目のポイントを完了するには、Implを作成するデフォルトのコンストラクターを実装する必要があります。
PimplPtr(): p_(std::make_unique<T>()) {}
Implにデフォルトのコンストラクターがない場合、コンパイラーはこれを言って、ユーザーは別のコンストラクターを必要とします。
explicit PimplPtr(std::unique_ptr<T>&& p) noexcept: p_(std::move(p)) {}
明確にするために、コンストラクタとデストラクタに静的チェックを追加する価値があるかもしれません。
PimplPtr(): p_(std::make_unique<T>()) { static_assert(sizeof(T) > 0, "Probably, you forgot to declare constructor explicitly"); } ~PimplPtr() { static_assert(sizeof(T) > 0, "Probably, you forgot to declare destructor explicitly"); }
また、移動のセマンティクスを保持するには、適切なコンストラクターと演算子を追加する必要があります。
PimplPtr(PimplPtr&&) noexcept = default; PimplPtr& operator =(PimplPtr&&) noexcept = default;
コード全体:
namespace utils { template<class T> class PimplPtr { public: using ElementType = typename std::unique_ptr<T>::element_type; PimplPtr(): p_(std::make_unique<T>()) { static_assert(sizeof(T) > 0, "Probably, you forgot to declare constructor explicitly"); } explicit PimplPtr(std::unique_ptr<T>&& p): p_(std::move(p)) {} PimplPtr(PimplPtr&&) noexcept = default; PimplPtr& operator =(PimplPtr&&) noexcept = default; ~PimplPtr() { static_assert(sizeof(T) > 0, "Probably, you forgot to declare destructor explicitly"); } const ElementType* get() const noexcept { return p_.get(); } const ElementType* operator->() const noexcept { return get(); } const ElementType& operator*() const noexcept { return *get(); } explicit operator const ElementType*() const noexcept { return get(); } ElementType* get() noexcept { return p_.get(); } ElementType* operator->() noexcept { return get(); } ElementType& operator*() noexcept { return *get(); } explicit operator ElementType*() noexcept { return get(); } private: std::unique_ptr<T> p_; }; } // namespace utils
使用例:
// widget.h class Widget { // ... struct Impl; utils::PimplPtr<Impl> d_; }; // widget.cpp struct Widget::Impl { /*...*/ }; Widget::Widget() {} Widget::~Widget() {}
設計されたポインターを使用すると、いくつかの愚かな間違いを回避し、有用なコードの作成に集中できます。
» ソースコード