Qtの循環参照でハマった話
仕事でQtを使うことがあった。
しかも参加者は全員QtもC++も初めてだった。
さらにやっつけで既に変な設計で書かれたコードが出来上がっていた。
「Qtってすごいらしいぜ。QObjectの継承クラスで親子関係を登録したら、あとは親をdeleteするだけで子も全部deleteするんだってよ」
「マジで!メモリ管理めちゃ楽じゃん。やったね」
なんて思ったのもつかの間。
「おい、なんか変なエラーでて落ちるんだけど。なにコレ」
「んー、ページングってことはメモリ関連じゃね?freeとtopでメモリ見てみ。あとsysteminfoで随時出力しといて」
「リークしてんじゃん。newとdeleteくらいチェックしとけや」
newで確保されるのはヒープ領域だ。
この領域に確保されたメモリは自動では開放されない。
deleteが必要なのだ。
そんなことは当然みんなわかっているので、deleteのし忘れなどない。
「チェックしたけどちゃんと対になってるぞ。何が原因だ?」
「継承元のデストラクタはvirtualついてる?」
「そこもチェック済み。」
「じゃあcppcheckっていう静的解析ソフトあるからつかってみたら」
「やってみた。けど駄目だった。ちょろっとwarningはでるけど、リークは見当たらないぞ」
「PThredとか別プロセスはどうだ?デタッチ忘れとか」
「いや、それも大丈夫だわ。問題なかった」
ちなみにQtCreatorのデバッグ機能や高機能なメモリリーク検出ソフトは使えない状況だった。
そんなわけで原因がわからないまま時は流れた。
そしてみんなQtにも少し詳しくなってきた。
「Qtってリファレンスカウンタがあって、それでdeleteを監視してるみたいだぞ。リークの原因これじゃね?」
「でもデストラクタ呼ばれてるからdeleteはされてるだろ」
「でもメモリは戻ってないからQtが内部的に領域を保持してる可能性もあるんじゃ」
「コピーオンライトで参照カウンタが減ってない可能性もあるかも」
「じゃあ試しにこのクラスの継承してるQObject切ってみたら?」
「あ、リークなくなった」
「「コレが原因か!?」
QObjectでの親子関係の参照と、子から親へthisの自己参照を渡して処理を委譲している部分があった。
これは親と子が互いの参照を持つ相互参照または循環参照と呼ばれる。
メモリが開放されなくなる最悪なパターンの一つだった。
普通は循環参照はコンパイルエラーになるはずだが、サブクラスとしてQObjectがあるので、キャストして参照の受け渡しをやっていたのだ。
アプリの設計が悪いのだが、プロトタイプを最初に作った人を責めても仕方がない。
いい勉強になったし、次はこのような設計にはしないだろう。
ちょっとだけ愚痴。
オライリーのQtの参考書には循環参照とかその辺りのことは一切載っていなかった。
結構陥りやすいミスだと思うので一言くらい解説があってもいいと思う。
QWeakPointerとかも載ってなかった。トラブルシューティングこそ載せるべきだと思う。
それからdelete thisはやめてほしいと思う。危険過ぎる。スコープを抜けるときにスタック領域の開放とかがあったら落ちるだろうし。
あとコードのコピペもやめてほしい。3回同じコードを書いたらリファクタリングしろと言いたい。
追記:
デストラクタのvirtualの記述を追加。
下記記事は参考になった。
C++の落とし穴