仮想デストラクタの是非

Home > 2005-08 / 実装技術 > This Entry [com : 0][Tb : 0]

2005-08-18

 「C++では、デストラクタは原則としてvirtualを付けて仮想関数にしておくこと。仮想でないデストラクタは継承できないことを意味する」との主張をよく目にすることがあります。今回は、組み込み環境でもそうした主張を鵜呑みにしてもよいかどうかについて、お話したいと思います。

確かに正攻法のオブジェクト指向プログラミングをやろうとすればその通りなのですが、仮想デストラクタというのはインスタンスを動的に生成することを前提としています。ご存知の通り、組み込みでは動的なメモリ管理は極力避ける傾向にありますから、ちょっと事情が違ってくるようです。

組み込みでは、ほとんどのクラスのインスタンスは、安易にnewで生成することはないと思います。newで生成することはあっても、どちらかといえば例外的な存在なのではないでしょうか?もしそうであれば、問答無用で仮想デストラクタを使用することは得策ではありません。

 仮想デストラクタ(というより仮想関数)は、newでインスタンスを生成することがあまりない環境では、純粋なオーバーヘッドになってしまいます。

まず、仮想関数は、ほとんどの場合インライン展開されることはありません。原理的にはインライン展開できそうな場合でも、現存するコンパイラの多くはインライン展開しないようです。多くの仮想デストラクタは、何の処理も行わないにも関わらずにです。

次に、仮想関数(仮想デストラクタを含む)がひとつでもあれば、多相クラスになってしまいます。多相クラスは、実際に使用されるかどうかに関わらず、実行時型識別情報も一緒に生成されてしまいます。また、実際には呼び出されない仮想関数も、基底クラスにまでさかのぼって、すべてリンクされてしまいます。

さらに、(仮想であろうがなかろうが)明示的なデストラクタを定義したクラスの自動オブジェクトを生成した場合、そのブロック内で例外が送出されることに備えて、非常に大きなサイズの巻き戻し情報が生成されてしまいます。

 このように、闇雲に仮想デストラクタを使うと、プログラムサイズが肥大する上に、ソースコードからは見極めにくい内部処理が少なからず発生してしまいます。こうした影響は、いかにも組み込み向きではありません。では、仮想デストラクタに代わる方法はないのでしょうか。

仮想デストラクタを使うのは、派生クラスのインスタンスを指す基底クラスのポインタを用いてdeleteした場合に、派生クラスのデストラクタが正しく呼び出され、かつ適切な解体関数(operator delete)が呼び出されるようにするためです。

仮想デストラクタをなくすには、基底クラスのポインタを使ってdeleteできないようにするか、仮想デストラクタ以外の方法で、正しい解体処理が行われるようにしなければなりません。あるいは、派生できないクラスを作るかです。

 基底クラスのポインタを使ってdeleteできないようにするには、基底クラスが、抽象クラスの場合と同様に、それ自体のインスタンスを生成できないのであれば、デストラクタをprotected部で定義すれば実現できます。しかし、この方法では例外処理用のコード生成を抑止することはできません。

例外処理用のコード生成を抑止するために、明示的なデストラクタの定義を行わないためには、newおよびdeleteをprivate部で宣言することで実現できます。この方法は、派生クラスでnewとdeleteを明示的に多重定義する必要がありますが、簡単かつ確実です。

class A
{
public:
    ...
private:
// newとdeleteを隠蔽する
// ここではnewとdeleteを一組宣言すれば、newとdeleteが全て隠蔽できる
    static void* operator new(std::size_t);
    static void operator delete(void*);
};

class B : public A
{
public:
// 派生クラスでは、面倒でもnewとdeleteを一式多重定義する。
    static void* operator new(std::size_t size)
    {
        return ::operator new(size);
    }
    static void* operator new(std::size_t size, const std::nothrow_t& nt)
    {
        return ::operator new(size, nt);
    }
    static void* operator new[](std::size_t size)
    {
        return ::operator new[](size);
    }
    static void* operator new[](std::size_t size, const std::nothrow_t& nt)
    {
        return ::operator new[](size, nt);
    }
    static void* operator new(std::size_t size, void* p)
    {
        return p;
    }
    static void* operator new[](std::size_t size, void* p)
    {
        return p;
    }

    static void operator delete(void* p)
    {
        ::operator delete(p);
    }
    static void operator delete(void* p, const std::nothrow_t& nt)
    {
        ::operator delete(p, nt);
    }
    static void operator delete[](void* p)
    {
        ::operator delete[](p);
    }
    static void operator delete[](void* p, const std::nothrow_t& nt)
    {
        ::operator delete[](p, nt);
    }
    ...
};



 仮想デストラクタ以外の方法で正しく解体できるようにするには、ちょっとしたトリックが必要になります。C++の準標準的な位置付けのライブラリであるBoost C++ Librariesには、これを可能にするshared_ptrという一種のスマートポインタが含まれています。

shared_ptrの主な目的は、参照カウンタを用いて、インスタンスの解体時期を制御することですが、仮想デストラクタを持たないクラスでも正しく解体できるような工夫が施されています。以下にその仕組みを簡単に説明します。(実際の実装より簡略化しています)

template <typename T>
void deleter(void* p)
{
    delete static_cast<T*>(p);
}

struct shared_count
{
    shared_count(void* p, void (*pf)(void*))
     : ptr(p), func(pf) {}
    void* ptr;
    void (*func)(void*);
};

template <typename T>
class shared_ptr
{
public:
    template <typename U>
    explicit shared_ptr(U* p)
     : px(p), pn(new shared_count(p, &deleter<U>) {}
    ~shared_ptr()
    {
        (*pn->func)(pn->ptr);
        delete pn;
    }
    T* operator->() const
    {
        return px;
    }
private:
    T* px;
    shared_count* pn;
};



ざっと説明すると、share_ptrのコンストラクタにはインスタンスの実際の型のポインタ(普通はnew式そのもの)を渡します。そして、その際に実際の型に対応した買いたい関数と、ポインタの値を保持しておきます。つまり、仮想デストラクタがやっていることを自前で実装しているわけです。

こうしたスマートポインタは、普通クラステンプレートにしますので、一度作ってしまえば何度でも再利用できますから、かなり便利かと思います。

 最後に、派生できないクラスですが、C++ではそうしたクラスを直接記述するための機能がありません。したがって、ここでも何らかのトリックが必要になります。

ひとつは仮想継承を使う方法ですが、難解な割にはメリットが少ないので、ここでは詳しく説明しません。もう一つは、共用体を使う方法です。こちらは簡単ですので、説明しておきたいと思います。

C++では共用体も一種のクラスです。メンバ関数を定義することもできますし、private部を作ることもできます。しかし、他の種類のクラス("class"や"struct")に比べると、いくつか機能が制限されています。

まず、共用体ではコンストラクタやデストラクタを持つクラスをデータメンバにすることができません。そして、他のクラスから派生することも、共用体から派生することもできません。この、共用体から派生できないという制約を逆に利用するわけです。

共用体に複数のデータメンバを持たせるには、一旦構造体にしてからデータメンバにすれば問題ありません。コンストラクタやデストラクタを持つクラスをデータメンバにできないのは難ですが、単純なデータしか扱わないクラスであれば、十分に利用できるのではないでしょうか。

 何か、非常に面倒そうに見えますが、newを使った動的な生成を行うかどうかわからないクラスを作ろうとすると、このような対処が必要になるわけです。

newを使った動的な生成しかしない場合は、素直に仮想デストラクタを定義した方がよいと思います。逆に、動的に生成することがなければ、何も面倒なことは発生しません。組み込み開発では、クラスの設計時に、それが動的生成の対象になるかどうかをはっきりさせておいた方がよさそうです。

Comment

Post a Comment









管理者にだけ表示を許可

Trackback

http://cppemb.blog17.fc2.com/tb.php/23-bcf1895a

C++と組み込み環境 | Page Top▲

New >>
スマートポインタ
<< old
C結合と多重定義
ブログ内検索
RSSフィード