読者です 読者をやめる 読者になる 読者になる

ぷろみん

プログラミング的な内容を扱ってます

基本クラスの継承を考えたデストラクタの属性設定

概要

この記事は間違った情報が多分に含まれるため

https://github.com/isocpp/CppCoreGuidelines/blob/master/CppCoreGuidelines.md#c35-a-base-class-destructor-should-be-either-public-and-virtual-or-protected-and-nonvirtual

https://github.com/isocpp/CppCoreGuidelines/blob/master/CppCoreGuidelines.md#discussion-make-base-class-destructors-public-and-virtual-or-protected-and-nonvirtual

の翻訳記事に書き換える予定です。

以下元の記事。

実行時の型とポインタの型が違う場合、ただnewするだけでは実行時の型情報がロストするので、ちゃんとtype erasureするかvirtualを付けましょう。

型情報がロストして困る例

#include <iostream>

struct Foo{
    ~Foo(){ std::cout << "Foo" << std::endl; }
};

struct Bar: public Foo{
    ~Bar(){ std::cout << "Bar" << std::endl; }
};

int main(){
    Foo *bar = new Bar();
    delete bar;
}

このコードではBarのデストラクタが実行されません。何故ならBarの型情報をどこにも保持していないからです。

virtualで解決する

struct Foo{
    virtual ~Foo(){ std::cout << "Foo" << std::endl; }
};

そこでデストラクタにvirtualを付けてやるとBarの型情報が(正確にはオーバーライドされた関数へのポインタ)が保持されるのでBarのデストラクタが実行されるようになります。

type erasureで解決する

int main(){
    std::shared_ptr<Foo> bar(new Bar);
}

std::shared_ptrtype erasureというテクニックを使い型を内部で保持します。なのでBarのデストラクタにvirtualが付いていなくてもBarのデストラクタが実行されます。

どっちを使うべきか

Fooインターフェイスの場合継承される意図を明確にする為にvirtualを付けるのが良いと思います。反対にFooがただの一般クラスで同じ機能を流用したくて継承する場合はvirtualを付ける事を推奨しません。こういう場合は使いたい機能をインターフェイスに分離すべきです。

インターフェイスが何らかの事情で用意できない場合ですらstd::shared_ptrで解放するのは意図が明白にならないのでオススメしません。継承先解放用のクラスを作成するのが本来なら良いのですが、このような複雑なテンプレートはしっかりとテストできる環境でのみ作成すべきです。なので、妥協案として継承先解放用のクラスをshared_ptrのエイリアスとして定義すると良いと思います。

余談

std::unique_ptrは型情報を内部で保存していません。なので、継承先のデストラクタは実行されません。