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