この前 Boost.勉強会 #2 で喋ってきたわけですが、
なにせああいった場での発表は初めてだったので、伝えたいことが伝えきれず、
また「口頭で触れたけど、スライドには書いてない」事柄が多かったので、
この場を借りて補足させていただきたいと思います。
が、このような補足は、本来は行うべきではないと考えているので、
今度どこかで発表する機会があれば、補足をする必要のない発表をしたい次第。
この記事は上記のリンクから見られるスライドを参照しながら読むことを前提としています。
また、気がついたことがあり次第 更新するかもしれません。
9/17: 「一時オブジェクトに対する参照」を追記
自己紹介
「天然でも人工でもありません、野良C++erです。」要はこれが言いたかっただけ。
なお「野良C++er」の意味には「今現在、何の開発にも携わってない」ことも含めてます。
中級以上のC++erである以上、何かの開発に携わりたいものですが…ご主人様募集中。
発表内容
実際の発表内容とはあまり関係ないです。わざとです。
導入
記載されてる英文は 公式ページTOP の冒頭二文のコピペです。
引用元を明記すべきでした…。
const
素敵です。ではなく。
定数を表すためのconstやconst参照渡しは、恐らく殆どのC++erは理解してる、
というか、理解してないとC++でコードを書くのは難しい訳ですが、
constの真骨頂はローカル変数につけるconstです。
この「ローカル変数にconstを付ける」ことを、ついったー上では「const教」などと大仰に表現していますが、
実際にはそこまで大仰なものではありません。「良い習慣」と呼んでいいレベルです*1。
そのメリットは、沢山ある(バグの防止、最適化)わけですが、一番のメリットとしては、
「constを付けることで『この変数は変更されない』という意図を伝えられる」ことにあります。
つまり可読性の問題です。後でコードを読み直すとき、constがあると理解の助けになるのです。
よく「C++は可読性が悪い」と揶揄されますが、クラスとテンプレートとconstのあるC++は、
(正しく書く限り)かなり可読性の高い言語だと思うのですが、今回は割愛。
int const n = 100; int a[n];
static const int では? と思うかもしれませんが、 static がなくても n は定数式です:
「最初に設定された値を保ちますよ」
ローカルconst変数の話です。
for( int i = 0; i < 10; ++i ) { int const x = std::rand() % 6 + 1; std::cout << x << std::endl; }
この x はループ毎に違う値を取りますが、有効期間中は同じ値を保ちます。
while( boost::optioal const line_ = getline_opt( std::cin ) )
while や if の条件部分には宣言を置くことが出来ます。
主に optional やスマートポインタで使う手法であり、簡潔で扱いやすいので愛用しています。
一時オブジェクトに対する参照
一時オブジェクトを const& で参照した場合、そのオブジェクトの寿命は延長されます。
C++0x の場合は && (rvalue reference)で参照しても同様です。
{ std::string const& x = "hoge"; // 一時オブジェクトへの const reference std::string && y = "fuga"; // 一時オブジェクトへの rvalue reference y = x; // OK, x も y も生きてる } // x と y が参照している一時オブジェクトはここで破棄される
が、意味が取りにくくなるので、個人的には推奨しません。特に const& 。
参照先が変更されることは普通に起き得る
マルチスレッドでなくても普通に起き得ます。ローカル変数でも普通に起き得ます。
例えば参照キャプチャしたラムダ式内でローカル変数の値を変更してた場合など。
これらは多くの場合うっかりミスです。普通は気付きますが、気付けない場合も多いです。
イテレータや関数オブジェクトは値渡しが基本。
部分的にconstな型
T const* の代わりに boost::optional
同様に T* const は boost::optional
またスライドで挙げた std::unique_ptr
実際に非 const オブジェクトへの参照を保持していることは稀です*3。
「変更しない」と「変更されない」は割と別物
C++ の const はそれらを一緒くたに扱ってるわけですが、「むしろそこが良い」と思う人も多いはず。
少なくとも僕は C++ の const に対して不満はありません。 immutable も欲しいのは確かですが。
それから「部分的なconstは不変性を崩す」とありますが、厳密に考えれば、別に崩れてません。
これは「不変性」の定義にもよりますが、例えば shared_ptr
「shared_ptrそのもの」と、「shared_ptrに所有されているオブジェクト」は、厳密には別物だからです。
とはいえ、 Flyweight に格納する場合などに求められる「より強い不変性」に対しては、
C++ の非推移的な const は、あまり歓迎できる性質とは言えないです*4。
意味論的const性
説明するの忘れたので。要するに mutable の事です。
主に遅延評価やメモ化をするときに必要になります。今回の例でも使いました。
C++ のconstには、例えビット列的には不変でなくても、意味的にconstならばconstにできる機能があります。
「意味的にconst」というのを厳密に考えるのは、それだけで論文になりそうなくらい大変ですが、
大雑把には「constならば値は変わらない」「constならば共有できる」辺りから判断することになります。
で、これらはマルチスレッドになると扱いが少々面倒なのですが、普通に使う分には有用で、
それを実現するために使われるのが mutable です*5。
詳しくは後ほど具体例で説明したいと思います。
基本的にオブジェクトと変数は1対1対応
一時オブジェクトや動的確保されたメモリ上に構築されたオブジェクトは例外です。当たり前ですが。
immutable なオブジェクトは値を共有できる
というか、僕は「値を共有できるオブジェクトは immutable である」と考えてます。意味論的に。
C++ でも、殆どのオブジェクトに対して T const は immutable ですが、
T* とか shared_ptr
std::shared_ptr
C++ における GC といえば shared_ptr です。
サンプルでさらっと C++0x の機能( Variadic Templates や Perfect Forwarding )を使ってます。
これらの機能を使うのは、意味を最も簡潔に表すために必要だと判断したからです。
とりあえず immutable
std::make_shared
なお、この immutable
値を再束縛できる
この用途では boost::optional
単純な代入が無理なのはいいとして、 boost::in_place による再束縛も無理なのは、バグな気もします。
「immutable object への参照を保持するクラス」
通常 immutable object の話をする場合は、 GC のある言語がメインであり、
その場合には immutable object と通常のオブジェクトの差は「単に不変かそうじゃないか」だけですが、
C++ は値の言語なので、少しばかり違ったアプローチをしなければなりません。
結論から言うと、 immutable ならば値だろうと参照だろうと(効率以外は)変わらないですが、
参照にすることによって再束縛が可能になり、これは明らかに使いやすいので、
通常の C++ では、 immutable object を扱う場合は、 immutable object への参照を保持させます。
なお、再束縛可能、というのは、要するに operator= や swap が定義されている、ということです。
言うまでもなく、これらの操作は破壊的なのですが、参照される中身には影響を与えないですし、
再束縛を不可能にするにはconstを付加すればいいので、問題にはなりません。
等値のオブジェクトを一つの実体にまとめ上げてしまう
ここが一番大事なのに、強調を忘れてしまいました。
要するに Flyweight デザインパターンの肝はここです。
Flyweight でない場合、別々に作られたオブジェクトは、例え内容が完全に同じでも、別々の実体を持ちます。
Flyweight の場合には、別々に作られたオブジェクトであっても、内容が同じなら、実体は唯一です。
本当に唯一です。例外はありません。言い換えるなら、実体が違えば内容も「必ず」違う、ということです。
もちろん、通常はそのようなことはありません。実体が違っていても内容が同じ、ということは普通です。
しかし Flyweight ならば、実体と内容は、完全に一対一で対応します。
勿論、普通にオブジェクトを作ったのでは、このようなことは起こりえません。
つまり Flyweight は、普通にオブジェクトを作ってるわけではありません。
Flyweight では、オブジェクトを作る際、今までに作られた全てのオブジェクトを登録したテーブルを参照し、
もし作ろうとするオブジェクトと同じ内容のオブジェクトが既に存在していれば、
新しくオブジェクトを作らず、代わりにそちらを使うのです(無いようならば新しく作って登録します)。
こうすることにより、全ての等値なオブジェクトは、同じ実体を持つことになります。
こうすることのメリットとしては、まず、リソースを節約できることが挙げられ、
複雑でないオブジェクトを大量に扱う場合(例:プログラムの構文解析や行数の多いファイルの処理)では、
積極的にそれらのオブジェクトをまとめ上げることで、リソース使用量を劇的に改善できます。
それから副産物として、値が同じことを確かめる場合に、実体のアドレス比較のみで済むということが挙げられます。
非侵入的
僕の考える「Boostの最も素晴らしい点」です。
shared_ptr, intrusive_ptr, unordered_***, assign などなど、
Boostは基本的に「Boostの為に作られたもの」でなくても扱えます。素晴らしい。
GCの方法
詳しく説明しなかったので捕捉。現状は refcounted と no_tracking があります。
前者は参照カウントによるGC、後者はGCを行わない指定です。
通常はデフォルトの refcounted が無難ですが、速度の点では圧倒的に no_tracking が速いです。
ただ no_tracking では、(実装依存の汚い方法を使わない限り)確保したメモリは解放されません。
実装を見て hack すれば確保したメモリを解放することは可能ですが、極めて危険ですので自己責任で。
これは現状「どのオブジェクトが生きているか」を調べる方法が存在しないためで、
仮にオブジェクトの生存をチェックしようと思ったら、現状ではデストラクタに頼るしか無く、その場合は参照カウントを使ったほうがいいからです。
C++0x で最小限のGCサポートが入れば、この辺りは改善できるはず。
パフォーマンス上の特徴
公式ページ: http://www.boost.org/doc/libs/1_44_0/libs/flyweight/doc/performance.html
意外と構築時のコスト増加はそれほどでもないようです。
そして特筆すべきは no_tracking の圧倒的な速さ。
実際に使ってみる
std::hash
は誤り。正しくは std::hash
です。
専用のラッパークラス
どう考えても専用のラッパークラスを作るほうが面倒です。
が、こちらは「一度作ってしまえば便利に流用できる」点があります。
とはいえ、ライブラリにするなら Boost.Flyweight を使わずチューニングするべきなので、
「とりあえずの実装」として使う程度でしょう。
trie を使った実装は、本当、誰かやってくれませんかね。
Key-Value Flyweight
公式のチュートリアル: http://www.boost.org/doc/libs/1_44_0/libs/flyweight/doc/tutorial/key_value.html
スライドにもあるように、これの本質は「遅延構築」にあります。
一般に構築の遅い Boost.Flyweight ですが、 Key-Value Flyweight を使うことで、
「既に実体が存在する」場合に限り、構築時間を大きく改善することができます。
とはいえ boost::flyweight
また、今回は触れませんでしたが、 Key-Value Flyweight の Key として boost::flyweight
Flyweight の「ハッシュテーブルのキーとして高速に扱える」性質をうまく使えます。
boost::filesystem::path
スライドでは言及してませんが、 Boost.Filesystem の v3 を使ってます。
Flyweight な boost::filesystem::path というのも悪くない気がしますね。
ファイルロードの実装
C++0x のラムダ式、およびラムダ式の関数ポインタへの暗黙変換を使ってます。
C++98/03 では関数内クラスの static メンバ関数を使うことで実装できます:
void lua_sourcefile_body::load( lua_State* L ) const { std::string const filename = path_.string(); if( up_to_date_() ) { luaL_loadbuffer( L, &chunk_[0], chunk_.size(), filename.c_str() ); } else { luaL_loadfile( L, filename.c_str() ); chunk_.clear(); struct dump_f { static int apply( lua_State* L, void const* p_, std::size_t sz, void* ud ) { std::vector<char>& chunk = *static_cast<std::vector<char>*>(ud); char const* const p = static_cast<char const*>(p_); chunk.insert( chunk.end(), p, p + sz ); return 0; } }; lua_dump( L, dump_f::apply, &chunk_ ); timestamp_ = last_write_time(path_); }
スライドで発表する都合上、一つの関数にまとめてますが、本来的には lua_dump をラップした、
// スタックトップの関数を std::vector<char> にダンプして返す inline std::vector<char> dump_to_vector( lua_State* L ) { std::vector<char> vec; struct dump_f { static int apply( lua_State* L, void const* p_, std::size_t sz, void* ud ) { std::vector<char>& vec = *static_cast<std::vector<char>*>(ud); char const* const p = static_cast<char const*>(p_); vec.insert( chunk.end(), p, p + sz ); return 0; } }; lua_dump( L, dump_f::apply, &vec ); return vec; }
のような関数を使って実装したほうがベターです。
なお、この関数が const なのは、 Flyweight にする都合もありますが、なにより「意味的にconstだから」です。
ロードという処理を行っているので非constだ、と考える人もいるかも知れませんが、
この処理の非const性を一手に担ってるのは lua_State* であって、ローダ自身ではありません*7。
確かにこの処理はローダ自身の状態も書き換えますが、それはあくまで「実装の詳細」であって、
このクラスが行う処理内容は、意味論的には「対象のファイルをロードしてスタックに積む」であり、
その処理はローダが保持する情報(つまりファイル名)を決して書き換えないのです。
この関数に関しては、今のファイルの状態に処理内容が依存するので、厳密にconstと言い切ることは難しいですが、
それでも、この処理をconstと表現することは、 Flyweight でなくても一定の説得力がある、ということです。
ただ、このように意味論的constを採用してmutableを使った場合、問題になるのがマルチスレッドで、
通常「変更されない」オブジェクトはマルチスレッドに強いわけですが、
mutable の使用はその利点を奪い去ってしまい、思わぬエラーを生み出す場合があります。
そのため、実際にはマルチスレッドに備えて、ロックをポリシー等で用意する必要があるのですが、
今回は mutable がメインではないので、省略させて頂きました。
PImpl イディオムと組み合わせる
sizeof(boost::flyweight
つまり void*& から boost::flyweight
これと placement new とか pseudo destructor call とかを組み合わせれば、
動的メモリ確保を省略することによってパフォーマンスを(多少)改善できます。
もちろん reinterpret_cast の使用は、分かってる人が、自己責任で行ないましょう。
補足は(今のところ)以上です。
以降は反省点を個人的にメモっておきます
見ても何の為にもならない雑多なメモなので、スルーを激しく推奨。
事前準備とか
明らかに準備が足りなかった。
その結果、発表を始めるときにもたついたり、後半の内容がすごく駆け足になってしまった。
前日までにスライドを仕上げ、軽く通しで確認しておけば、もっともっと良い内容に出来たはず。
発表の質を比較的 簡単に上げるには、やはり準備が大事だということを痛感するとともに、
分かってたはずの自分の見通しの甘さを再認識させられた。
スライドについて
他の人のスライドは凄く凝ったものが多かったので、見習いたい。
また、図がもっと欲しいという指摘を頂いた。その通りだと思う。
特に今回のお題である Flyweight デサインパターンに関しては、図に表すと一目瞭然だし。
ソースコードに関しては、どのようにまとめるべきか、あまり見当がつかないので、
今後の課題にしていきたい(少なくとも口頭で解説する場合はメモが必要)。
発表の態度とか
初めての発表とはいえ、あんまりだと思った。この場で言い訳を兼ねて改善点を挙げてみる。
まず、あがり症。これは昔からの性質で、今ではマシになった方であるが、
可能な限り挙動不審にならないように意識する、でもそればっかりに気を取られないように、
という方法を、更に徹底していく必要を痛感した。
それから滑舌の悪さ。これは元が早口なのと、普段はあまり人と喋らないのが原因。
出来る限りゆっくり大きな声で発表しようとしても、前述のあがり症もあって、ついつい早口+つっかえがちに。
これは準備の悪さから「何を言うかをその場で考えながら喋る」必要に迫られたのも大きな原因。
対処法は二つ、普段からゆっくりはっきりした声を意識して積極的に人と話すこと、
それから準備をしっかりして(可能ならば台本を用意して)喋ること自体に集中できるようにする点。
最後に体力。
発表が終わったとき、あまりの疲れに行動不能になった。体力があれば、もう少し余裕が持てた筈。
特に喉や舌の持続力が足りない感じで、これは普段からあまり喋らないのが大きい。
はっきり喋るように意識してたので余計に疲れたわけだが、そんなのは言い訳にならない。
リハーサルを兼ねて通しで練習しておけば体力配分は分かったはずなので、やはり準備不足が痛いか。
総じて、しっかり準備することと、もう少し社会に出ることが必要に思えた。
const
正直やりすぎた。今では反省している。
ああなってしまった原因は Flyweight に対する情報収集が少なかったせい。
内容的にはアレでも悪く無いと思ってるが、少し初心者向け過ぎた。
もっと気合を入れてソースコードを読み込んでいれば、もっとコアな内容も出来たはず。
具体的には、実装関連のメタプログラミング手法なども紹介できた。反省。
それから、初心者向けとしても失格で、
もし初心者向けの発表をしたければ、大事なことは二度と言わず何度でも繰り返して強調すべき。
口頭で強調したつもりになったら駄目で、やはりスライドでしっかり強調したほうがいい。
大事なことなので繰り返す。大事なことは二度と言わず何度でも繰り返せ。強調しろ。
初心者向け云々で、今思い出したが、言いたい事をかなりスルーしてしまっていた。
これは明らかにスライドの作り込みが甘い。言いたいことはスライドにメモった方がいいようだ。