経緯
ゲームを制作していました。僕は新たな変更をしていました。
データが幾らか含まれたクラスをテスト用にダミーでローカル変数で置いておいたのをメンバ変数に引っ越しました。これが悲劇の始まりだったのです……。
ソース
↓に事故ったプログラムの簡易版を上げておいたので、見たい人は見て下さい。以降は例プログラムといいます。
例プログラムの説明
Visual Studio 2019 のプロジェクトです。
class Base
を継承する class Child
があり、その中には class Datas
メンバ変数があります。
この class Child
は、 class MyMain
の中で Base * 型
として保持しています。
class MyMain
に Base *
メンバ変数を初期化する関数、解放する関数が含まれます。
class Base
にデストラクタは含まれず、 純粋仮想関数
が含まれます。
class Child
には virtual デストラクタ
が含まれます。
クラス名の適合性は気にしないでね。
何が起こるかな
Windows 10 Pro x64
, Visual Studio 2019
, Debug x86
で検証します。
例プログラムを実行すると、
Datas のコンストラクタ
Child のコンストラクタ
と表示されます。アサートが出ることも無く、デストラクタが呼ばれずにメモリリークしまくって正常終了(は?)します。
class Datas
も class Child
もデストラクタが呼ばれずに、 class Datas
のメンバ変数が解放されずメモリリークするという具合です。
派生クラスのデストラクタの復習
基底クラス型として動的確保した派生クラスのヒープのポインタを使用した時に、基底クラスのデストラクタも派生クラスのデストラクタも呼び出す為には、どちらのクラスのデストラクタにも virtual
修飾子を付加しますね。あとは自分で調べてくれ(丸投げ)。
作ってるゲームで起こった異変
『経緯』で行った操作をした結果、前バージョン(GitHub で管理しています)で起こらなかったメモリリークがなんか大量に発生しました。メモリリークは malloc()
や new
で動的確保した場合にしかふつう発生しないのですが、僕はC言語スタイルで(※ヒープの管理を怖がって new/delete
をさぼってるだけ)実体を扱うようにしています。なので、データクラス内の STL の解放失敗が原因だとすぐに察しました。
で、解放する為にデストラクタで解放関数を呼び出すようにしました。
デ ス ト ラ ク タ が 呼 び 出 さ れ な い で は あ り ま せ ん か !
※↑こういうことに使う機能ではありませんね。失礼しました。
解決方法
基底クラスにも virtual デストラクタ
をちゃんと実装しましょう。……それだけでした。
いや、あのですね(言い訳タイム)、『何が起こるかな』で挙げたように、基底クラスに virtual デストラクタ
が無かったにも関わらずアサートが出ずにアプリケーションが終了していたんですよ。なので全然気付かなかったんですよ。多態性クラスのデストラクタは virtual
にしなければならないことも知っていましたが、まさか基底クラスにも virtual デストラクタ
をちゃんと実装しなければならないだなんて思っていませんでした。
**しかも肝になるのが基底クラスの 純粋仮想関数
の存在です。こいつが無ければアサートがポップアップされます。**嘘みたいな本当の話です。基底クラスが抽象クラスだと、例プログラムの様にしたときに何食わぬ顔をして想定と違う動作をするのですよ。(因みに例プログラムで最後までどうすれば完全再現できるのかと悩んだ部分です。)
…まあ、要するに僕のミスです。俺が悪いんだよ。
まとめ
抽象基底クラスには必ず virtual デストラクタ
を実装する。というかコンストラクタとデストラクタはとくにやることが無くても忘れずに実装した方が良いと思います。
あとたちの悪いことに、抽象基底クラスに virtual デストラクタ
を実装し忘れると派生クラスのデストラクタすら呼ばれなくなります。恐らく、デストラクタに virtual
を付けない場合は基底クラスのデストラクタしか呼び出されないのですが、抽象クラスの場合は謎の力でデストラクタが デフォルト定義非仮想デストラクタ
として勝手に作られるんじゃないでしょうか。検索しても分からなかったし、関数の方はメモリレベルのデバッグ方法を知らないので結構大胆な予想になります。
【2021/04/30追記】
コメントでも頂きましたが、そういえば確かに、コンストラクタもデストラクタも暗黙定義はされるし仮想関数の方が高コストでした。暗黙定義のデストラクタはそういう理由で仮想にはならないようです。若干寝ぼけていたとはいえ、忘れていたのは結構痛く思います。
因みに、絶対使わないであろう面白豆知識も頂きました。
終わりに
なんやかんやあって予定が狂って大変だったところにこの事例が発生したので、今日(2021/04/29)は実質何も出来ませんでした。いやあ、折角の祝日というのに、一日無駄になった気がします…。
あと、記事を書いてる途中でタイトルは「抽象基底クラスにデストラクタを定義するのを忘れて事故った」にしようか迷いましたが、やっぱり変えないでおきました。