pimplイディオムを語る
foo.h:
class foo { ... };
bar.h:
class bar { ... };
があるとして
hoge.h:
#include "foo.h" #include "bar.h" class hoge { private: foo f; bar b; public: hoge(); foo& get_foo(); bar& get_bar(); };
hoge.cpp:
#include "hoge.h" hoge::hoge() {} foo& hoge::get_foo() { return f; } bar& hoge::get_bar() { return b; }
と書くより
hoge.h:
#include <memory> class foo; class bar; class hoge { private: struct impl; std::auto_ptr<impl> pimpl_; public: foo& get_foo(); bar& get_bar(); };
hoge.cpp:
#include "foo.h" #include "bar.h" #include "hoge.h" struct hoge::impl { foo f; bar b; }; hoge::hoge() : pimpl_(new impl) {} foo& hoge::get_foo() { return pimpl_->f; } bar& hoge::get_bar() { return pimpl_->b; }
と書くほうがすばらしいよという話。前者の場合はhoge.hをincludeした時点でfoo.hとbar.hまでincludeされてしまって、おまけにfoo.hやbar.hに変更が加えられてしまった場合にhoge.hはもちろんのことそれをincludeしているヘッダファイルまで変更が波及してしまうことになり、結果としてコンパイルが遅くなる。一方後者の場合はhoge.hの他への依存性はほぼゼロである。前方宣言を使うのは当然のことであるが、ここではそれ以上にpimplイディオムが大活躍している。つまりimplのポインタにfooやbarを格納してしまうことにより、それらのサイズや定義を知る必要がないのだ。ゆえにいくらhoge.hをincludeしているヘッダファイルがあろうともfoo.hやbar.hの変更はそれらに波及しえない。これによって変更に対する耐性が保証され、結果としてコンパイルが速くなる。
しかしpimplイディオムのメリットはそれだけではない。いやpimplの本質上、メリットがコンパイルの高速化だけでとどまること自体がそもそもおかしい。挙げだすと切りがないのだが(要するにstackにおくこととheapにおくこととの差によるメリット)、個人的に非常に興味深いのは例外安全に対する驚くべき能力である。たとえばfoo.hが以下のようなアホなコードだったとしよう。
foo.h:
class foo { public: ... foo& operator =(const foo& rhs) { throw not_implemented_error(); // how stupid I am! } ... };
このアホクラスを使ってテストコードを書く。
hoge a; hoge b; a = b;
このコードをpimplイディオムでないバージョンのhogeでコンパイルすると a = b の部分で例外が発生する。依存したクラスのおかげで自分のクラスまで非例外安全になるなんてなんてすばらしいのでしょう。しかしpimplイディオムの黒魔術にかかればそんな問題も一発。hoge.hとhoge.cppを以下のように改造する。
hoge.h:
#include <memory> class foo; class bar; class hoge { private: struct impl; std::auto_ptr<impl> pimpl_; public: foo& get_foo(); bar& get_bar(); hoge& operator =(const hoge& rhs); // never throw };
hoge.cpp:
#include "foo.h" #include "bar.h" #include "hoge.h" struct hoge::impl { foo f; bar b; }; hoge::hoge() : pimpl_(new impl) {} foo& hoge::get_foo() { return pimpl_->f; } bar& hoge::get_bar() { return pimpl_->b; } hoge& hoge::operator =(const hoge& rhs) { if (this != &rhs) { pimpl_ = rhs.pimpl_; } return *this; }
これで何回 hoge::operator = しようが決して例外が発生することはない。すばらしすぎる。
その他にも new impl_ するタイミングを調整できたり、 struct impl と本来のクラスに実装をわけて構成をわかりやすくしたり、(時と場合によるが)本体のコピーやswapが断然速いなど、pimplにはたくさんのすばらしいメリットがある。ただもちろんそんなにおいしいことばかりでもなく、heapを使う分のパフォーマンス低下は当然考えられる問題だ。しかしメリットとデメリットを計算したとき、最終的にはやはり符号は+になることだろう。
- The URL to Trackback this entry is:
- http://dev.ariel-networks.com/Members/matsuyama/pimpl30a430a330aa30e030928a9e308b/tbping
Re:pimplイディオムを語る
Re:pimplイディオムを語る
pimpl_ = rhs.pimpl_;
は
delete pimpl_;
pimple_ = rhs.pimple_;
ってことになってOKじゃないですか?
Re:pimplイディオムを語る
auto_ptr
{
auto_ptr
b = a;
//ここでbがデストラクタされる、ポインタ先を解放する
}
//ここの時点で、aはすでに解放された不正なポインタ指してる。
ってな具合で。
Re:pimplイディオムを語る
pimpl_ = rhs.pimpl_;
は
pimpl_.reset(rhs.pimpl_.release());
だから
auto_ptr a(new Hoge());
{
auto_ptr b;
b = a; // ここでaはbに所有権を委譲し,aは0ポインタを指す.
// ここでbのデストラクタが呼び出され,ポインタ先を解放する.
}
// ここの時点で,aは0ポインタ指している.
ってな具合でここの動作に関しては問題ないと思います.
Re:pimplイディオムを語る
代入してpimpleの所有権が移ってしまうと、元のオブジェクトが持ってたpimpleはどこへ?
boostを使ってよいのなら、pimpleにはboost::scoped_ptr を使うべきです。
#ついでに、pimpleにboost::shared_ptrを使うのも危険です。うっかりしたoperator=を実装すると、複数のオブジェクトで同じpimpleを共有してしまうので。
Re:pimplイディオムを語る
Re:pimplイディオムを語る
pimpl_ = rhs.pimpl_;について、2009-05-15 13:48のAnonymous Userさんやhiroseさんのおっしゃっているのと同じことを指摘するつもりで最初にコメントを書きました。曖昧な記述で混乱を招きましたら、申し訳ございませんでした。ただ、今になって気付いたのですが、実際にはrhsがconstのためコンパイルエラーになりますね(代入元がconstだとauto_ptrはエラーになります)。どちらにせよ、「参照カウントではなく所有権の移動という挙動を持っているためauto_ptrでは問題がある」ということに変わりはありません。
さらに、最初にコメントをしたとき私はまだ知りませんでしたが、auto_ptrを使ってはならないもう1つの理由があります。fooやbarのデストラクタ(根本的にはhoge::implのデストラクタ)が呼ばれないのです(こっちのほうがよっぽど深刻!)。pimpl_のデストラクタで、hoge::impl(不完全型)へのポインタに対しdeleteするのがその原因です。なお、boost::scoped_ptrだとこれを検出してコンパイルエラーにしてくれます。
これの対処は次のいずれかで可能です。
1. boost::shared_ptr / std::shared_ptrを使う: 動的削除子 (dynamic deleter) - 意外と知られていない??boost::shared_ptr の側面 ― Cry's Diary
2. hogeに明示的なデストラクタの定義を与える(boost::scoped_ptr / std::auto_ptrで適用可能): pimplイディオムをscoped_ptrを使って実現するときの注意 ― redboltzの日記