簡単に Pimpl を使ってみる
名前は知っていたけど、使ったことがなかったので、試しに使ってみた。
Pimpl とは
にきび(pimple)ではありません。Pointer to Implementation
(実装へのポインタ)を略して、Pimpl
です。
このテクニックによって、プライベートな詳細を開示せずに済みます。つまりインタフェースと実装を切り離す方法です。
これは、実際にコードを見たほうが早いと思うので、先にコードを出します。
実装
とりあえず以下のようなコードを書いてみました。これは、まだ Pimpl を使っていません。
// Widget.h #pragma once #include <stdint.h> class Widget { public: Widget(); ~Widget(); int32_t getNum(); private: int32_t num_ = 32; }; // Widget.cpp #include "Widget.h" #include <iostream> Widget::Widget() { std::cout << "Constructor" << std::endl; } Widget::~Widget() { std::cout << "Destructor" << std::endl; } int32_t Widget::getNum() { return num_; }
Widget::getNum() を呼ぶことで、内部の num のデータを取れるクラスです。このクラスを使う側(ユーザ)は、データの取り方が分かればいいので、変数 num があることを知らせる必要はありません。なので、これを実装ファイル(Widget.cpp
)に入れてやります。
// Widget.h #pragma once #include <stdint.h> class Widget { public: Widget(); ~Widget(); int32_t getNum(); private: class Impl; Impl* impl_; }; // Widget.cpp #include "Widget.h" #include <iostream> class Widget::Impl { public: Impl() { std::cout << "Impl Constructor" << std::endl; } ~Impl() { std::cout << "Impl Destructor" << std::endl; } int32_t num = 32; }; Widget::Widget() : impl_(new Impl()) { std::cout << "Constructor" << std::endl; } Widget::~Widget() { std::cout << "Destructor" << std::endl; delete impl_; impl_ = nullptr; } int32_t Widget::getNum() { return impl_->num; }
まずヘッダから見ていきます。private
にメンバ変数 num
がありましたが、それが消えて、代わりに クラス Impl
の前方宣言とそのクラス Impl のポインタ
を追加しました。
次に実装側ですが、初めに クラス Imple
の定義を行っています。その中にさっきは、Widget.h
にあったメンバ変数が Widget::Impl
に移動しています。
Widget のコンストラクタ
の初期化子リストに impl_
が追加され、デストラクタでは、impl_ の開放
を行っています。最後に、Widget::getNum() は、impl_のメンバ変数 num をアクセスして、値を返すように変更されています。
このように、詳細を実装ファイル内に隠してしまうのが、Pimpl です。
Pimpl を実装するときの注意
Pimpl を書くときに、いくつか注意することがあります(C++ に慣れている方は大したことではないですが)。
まず、Pimpl となる変数(今回だと impl_
)の型は、ポインタ(Impl*
)でないといけません。当たり前ですが、ヘッダ側は、Impl
の実装内容を知らないので、Impl
のオブジェクトを持てません。
Impl の詳細を書くときは、必ず Impl を利用する処理よりも先に書くようにします。今回だと Widget::Widget()
よりも先に書く必要があります。これも Impl
の詳細を知らないと使えないためです。
最後に、デストラクタでは必ず Impl の変数を開放するようにします。ポインタなので、当然ですね。
Pimpl をスマートポインタにする
Pimpl をスマートポインタに変更することで、デストラクタ内で Pimpl を開放処理を書かなくて済みます。例えば以下のようにします。
std::unique_ptr<Impl> impl_;
デメリット
Pimpl を使うことでいくつかデメリットがあります。
- コンストラクタでオブジェクトの割当、デストラクタでオブジェクトを破棄する必要がある
- すべてのプライベートメンバは、Pimpl を通じてアクセスしないといけない
- Pimpl をプライベートにすると、Pimpl を持つクラス(Widget)しか Pimpl のメンバにアクセスできない
- プライベート仮想メソッドを作れない。オーバライドする場合、パブリックにして、派生クラスがオーバライドできるようにしないといけない
- Pimpl(impl_) から、それを持つクラス(Widget)のパブリックなメンバ変数にアクセスするには、Pimpl にそのクラスへのポインタを追加するか、そのメソッドにクラスを渡す必要がある
- const メソッド内で Pimpl(impl_)のメンバ変数を変更してもコンパイラは検知することができない
いくつかデメリットはありますが、実装する側のデメリットであり、利用者に対してのデメリットはないので、使わない理由にはならないでしょう。
メリット
次は、メリットです。
- ユーザ側が見ることになるヘッダファイルの内容がすっきりする。
- 実装の詳細をユーザ側に見られることがなくなり、不正な方法でアクセスすることもできなくなる。
- ヘッダのインクルードファイルが減るので、結合度が減る。
- コンパイル時間が高速化される
- バイナリ互換性が向上する
- Pimpl は要求時に作ることもできるので、ネットワーク接続など制限のあるリソースやコストの高いリソースの割当のときに役に立つ。
などなどあります。詳細は、参考にした本を見てください(最後に載せています)
コピーセマンティクス
Pimpl を持つクラスをコピーした場合、当然 Pimpl もコピーされる。この状態で、コピー元か、コピー先のオブジェクトを開放したとき、もう片方のオブジェクトが Pimpl にアクセスすると Pimpl はすでに開放されているため、エラーが発生します。
これを防ぐためには、3つの方法があります。
個人的には、スマートポインタが楽だと思います。コピーを禁止したい場合は、std::unique_ptr
, コピーを許可するなら std::shared_ptr
でいいと思います。
スマートポインタを使えば、コピーセマンティクスを書く必要もなくなります。
おわり
Pimpl は、有効に使えると強力な反面、注意する点が多い方法だと感じました。デメリットは小さいので、積極的に使っていきたいですが、プライベートをなんでもかんでも Pimpl に実装するのも違うと思うので、よく考えて使いたいと思います。
Pimpl の書き方をメインに書いたので、どう活用するかは分かりにくかったかもしれません。そういった内容は、参考にした本に載っているのでそちらを参照するといいでしょう。
今回のコードは、以下に上げています。
参考にした資料です。
- 作者: マーティン・レディ
- 出版社/メーカー: SBクリエイティブ
- 発売日: 2013/11/15
- メディア: Kindle版
- この商品を含むブログを見る