実際にプロダクトにC++11を投入した際、これは便利だと使い始めたC++11の機能に関してあとからウッとなってきたので共有を。
ラムダ式の使い方にはおおまかに2種類ある
これはC++に限らないことで、ラムダ式(もしくは無名関数)をサポートする手続き型言語全般について言えることなのですが、ラムダ式の使い方は大まかに2種類です。
- map, filter, reduce をはじめとした高階関数に渡してその場限りで使ってもらう
- イベントハンドラとして渡していつかのタイミングで(ときには違うスレッドから)呼び出してもらう
前者、高階関数に使い捨てで与えるの、これは単純に便利です。がんがん使っていきましょう。
問題は後者、イベントハンドラです。
C++のラムダ式はクロージャの寿命を保証してくれない
C++というOOP言語を使っている以上は、イベントハンドラとして渡すようなラムダ式はほぼ確実に this
をキャプチャしていることと思います。よくあるGC環境向け言語なら this
がクロージャとなり、関数オブジェクトが死ぬまで親である this
オブジェクトも延命されます(これはこれでややこしいメモリリークの原因になるので厄介ではあるものの)。
ところがC++11ラムダ式がキャプチャした this
はあくまでアドレス値、これを知っているからと言ってオブジェクトが生存していることを保証してくれません。
せっかくC++11で整備されたスマートポインタでメモリアクセス違反を防いでいたつもりでも、ラムダ式が無効な this
を参照してしまい、イベントが発生した瞬間にクラッシュという悲劇が起こりうるわけです。
ラムダ式に this
そのものではなく shared_ptr
でくるんだスマートポインタを渡すのでは? いやそれは無理です。スマートポインタを一回だけ作って運用するのはオブジェクトの外側、利用者側でやることであり、オブジェクトの内部でも this
のスマートポインタを作ってしまったらそれは二重管理です。確実に二重deleteを起こしてクラッシュするだけ。
C++11以降のイベントハンドラ
オブジェクトの寿命管理を人間の注意力に依存しないというポリシーを守りつつイベントハンドラを合理的に設計するにはどうすべきか。前項で軽く否定したのですがやはり親オブジェクトを指すスマートポインタははずせません。 shared_ptr
を使って親オブジェクトの方を延命するか weak_ptr
を使ってイベントハンドラの方に勝手に死んでもらうかはケースバイケースとして。
このような方法を考えました。
- イベントハンドラはやはり
std::function
ではなくstd::shared_ptr<イベントハンドラインターフェース型>
(もしくはstd::weak_ptr<イベントハンドラインターフェース型>
)とします。 なんか昔のJavaプログラミングみたいですね。 - 親オブジェクトが自分自身を管理するスマートポインタを知ることができるよう、
●コンストラクタはprivateにし、新規作成時はstaticファクトリ関数でスマートポインタを得られる形にします。
●ファクトリ関数は新規作成したオブジェクトにもスマートポインタをweak_ptr
の形で与えます。 - 親オブジェクトはイベントにハンドラを仕込むとき、持っている
weak_ptr
か、それをロックしたshared_ptr
を渡します。
これで安全なイベントハンドラとして成立しているかと思います。 shared_ptr
を渡して親オブジェクトを延命していくスタイルだったら、それをキャプチャした std::function
を渡すのでもいいかも。でも親オブジェクトが例えばウィンドウビューとかだったら延命しても仕方が無いので weak_ptr
を渡してイベントハンドラに死んでもらう方のスタイルにしかなり得ませんね。
別解?
別解としては、普通にラムダ式を使って this
も参照しているイベントハンドラを仕込むんだけれど親オブジェクトのデストラクタで必ず仕込んだイベントハンドラをすべて取り下げるようにする、というのが考えられるんですが、これは⋯
- 結局人間の注意力に依存している
- マルチスレッドプログラミングだとデストラクタまわりがデッドロックの温床になりそう、また、デストラクタ周りをがちがちに排他制御で固めても絶妙なタイミングですり抜けて無効アドレスアクセスしてくる流れを完全には絶てなさそう
ということであまり良いやり方には思われませんでした。この辺、「○○そう」が多いのは軽く組んでみて「うっこれは辛い」とすぐに捨ててしまったのでもしかしたら良い実装があるのか完全に検証したわけではないせいです。パターンなどありましたらご教示いただけるとうれしいです。