C++ではデストラクタにvirtualをつけておかないとメモリリークするケースがある。下記のように基底クラスと派生クラスでそれぞれコンストラクタでメモリ確保、デストラクタで解放しているとする。
class Base {
private:
A a;
public:
Base(){ a = new A();}
~Base(){ delete a; } // ...1
}
class Child: Base { // Baseを継承してChildを定義
private:
B b;
public:
Child(){ b = new B(); }
~Child(){ delete b;}
}
Child型でdeleteすれば問題ない。
Child child = new Child();
delete child; ... OK!
ポリモーフィズムでBase型にChild型のインスタンスを入れることはよくあるのだが、これが問題になる。
Base base = new Child(); // あるいはFactoryからもらってくる
delete base; ... Memory Leak!!!!
この場合delete base
はコンパイル時にBaseクラスのデストラクタに静的に紐付いてしまい、~Base()
のみが実行される。Childの方はデストラクタが呼ばれずbのメモリを確保したままbaseへのアクセス方法を失う。Baseクラス定義の...1の部分をvirtual ~Base()
としていると、delete base
の呼び出し時に動的にbase
の現在のbaseクラスのインスタンスをチェックし、~Child()
が呼ばれてから~Base()
が実行されるので大丈夫である。わずかなメモリリークは単体・結合テストをすり抜けてしまう。今ではググればすぐわかる事だが、ネットがない時代ではきっと各社で同じ様なバグをだしては各々解決策を探すということをしていたと思う。このあとメモリカウンタ(newとdeleteの回数、呼び出した場所の集計)とかGCを持った言語の登場などで楽になっていくが、当時はみんな苦労していたのである。
ちなみに、デストラクタの件ではないが筆者も新人の頃に品質管理部門でのエージングテストで一週間後にメモリ逼迫して動作が遅くなるバグを出してしまい、始末書を書いたことがある。そこでは結合テスト以降にバグが見つかった場合は常に始末書だった。
このような時代背景の中でJavaは誕生した。Javaでは多重継承をなくしひし形軽症問題を解決し、機能継承は1クラスに限り可能とし、interfaceだけ複数継承可能とする(後に変更)ことで設計の綺麗さを保とうとした。複数の機能をも継承する場合はそもそも「ドメインモデルの設計自体が悪いのでは?」ということである。これは関数型言語が見直されて型クラスの有用さが認知され、「悪いのは実装を複数継承できることではなく、設計の問題」ということでinterfaceにデフォルト実装がついて型クラスもどきを実装できるようになった。
C++が流行りだした90年代、時代を席巻していたのはC言語だ。関数呼び出しオーバーヘッド一つでも削減したい。そんな思いからデフォルトではvirtualとならず、必要な箇所だけ自分でvirtual宣言する仕様となったのは良い。でも業務でチーム開発する場合は、「デストラクタには常にvirtualをつける」という運用ルールでいいのではなかろうか
class Base {
private:
A a;
public:
Base(){ a = new A();}
virtual ~Base(){ delete a; } // ...1
}
class Child: Base { // Baseを継承してChildを定義
private:
B b;
public:
Child(){ b = new B(); }
virtual ~Child(){ delete b;}
}
え?今更cppを選択するくらいならRustを使う?そりゃそうでしょう。でも世の中には古いソースをメンテし続ける環境もあるのです。
というかC++なんて何十年も書いてないのになんでこんな記事書いたんだろ。下書きにあったから供養のために公開しました。