uni/index テンプレートとデストラクタ

 テンプレート引数にクラスを指定する場合、引数に指定するクラスはデストラクタの有無が分かっていなければならない。 正確な表現じゃないけど、実用上は大体これでいいはず。 一番ありがちなのが、実装の隠蔽に使われるコレ。

#include <memory>

class CImpl;

class C {
  private:
    std::auto_ptr<CImpl> mpimpl;
};

void foo()
{
    C c;
}

 std::auto_ptrのテンプレート引数にクラスCImplを指定していますが、引数に指定したクラスCImplは前方参照宣言だけでデストラクタの有無が分かりません。 こんなのをコンパイルするとこうなります。

$ g++ -c -W -O -Wall test.cpp
/usr/lib/gcc/i686-pc-cygwin/3.4.4/include/c++/memory:
In destructor `std::auto_ptr<_Tp>::~auto_ptr() [with _Tp = CImpl]': test.cpp:12: instantiated from here /usr/lib/gcc/i686-pc-cygwin/3.4.4/include/c++/memory:260:
warning: possible problem detected in invocation of delete operator: /usr/lib/gcc/i686-pc-cygwin/3.4.4/include/c++/memory:260:
warning: invalid use of undefined type `struct CImpl' test.cpp:3: warning: forward declaration of `struct CImpl' /usr/lib/gcc/i686-pc-cygwin/3.4.4/include/c++/memory:260:
note: neither the destructor nor the class-specific operator delete will be called,
even if they are declared when the class is defined.

簡単に言うと「CImplのデストラクタは多分呼ばれないぞ」ということです。 これはg++での例ですが、MSVCでも「'CImpl' 型を削除するため delete 演算子が呼び出されましたが、定義がありません。」という(不親切な)警告が出ます。

22 Mar 2014

 Cygwin付属のgcc 4.8.2でやってみたら警告は出ませんでしたが、やはりデストラクタは呼ばれませんでした(GCCなのに・・・)。 -std=c++11でunique_ptrを使うとエラーになります。

 なぜデストラクタが呼ばれないのか? std::auto_ptrテンプレートクラスはデストラクタを持っています。 したがって、メンバC::mpimplはCの破壊時にデストラクタを呼ぶ必要があります。 しかし、クラスCはデストラクタを持っていません。

 こういう場合、メンバや基底クラスのデストラクタを順次呼び出す、デフォルトデストラクタが生成されることになっています。 デフォルトデストラクタがどう生成されるかについては「注解C++リファレンス・マニュアル(原題: The Annotated C++ Reference Manual、通称ARM)」では言及されていないようです。 少なくともC::~Cを外部から見えるシンボルとして定義してしまうのは明らかにマズいです。 クラスCを使う複数の翻訳単位があれば、それらの間でシンボルが競合してしまいますから。

 残された解決策は、ファイルスコープでデストラクタをコッソリ生成するか、インライン展開するか、です。 いずれにせよ、このファイルをコンパイルしているときにデストラクタが生成できなければなりません。 言い換えると、基底クラスと抱え込んでいるメンバについて、デストラクタを実行する必要があります。

 このとき、std::auto_ptrのようなテンプレートクラスでは、テンプレートの定義でデストラクタがインラインで定義されている場合、インラインでデストラクタを展開する必要があります。 std::auto_ptrの場合、テンプレート引数で指定されたクラスのオブジェクトをdeleteしますから、このクラス(この例ではCImpl)にデストラクタがあればそれを実行する必要があります。 前振りが長いですが、ここまでは問題ないです。

 問題は「CImplのデストラクタがあるのかないのかが分からない」、という点です。 CImplの定義にデストラクタがインラインで書かれていれば、それはきっとインライン展開されるでしょう。 デストラクタの宣言だけが書かれていれば、デストラクタの呼び出しに展開されます。 書かれていなければさらにCImplの基底クラス・メンバのデストラクタを呼び出すようなデフォルトデストラクタを作らなければなりません。 しかし、前方参照宣言「class CImpl;」だけではそのどれになるかがまったく分かりません。 基底クラスやメンバについてもまったく分かりませんね。

 仕方がないのでコンパイラは「デストラクタもなければ、デストラクタ呼び出しの必要のあるメンバ・基底クラスもない」としてコードを展開します。 既に展開してしまっているので、後からデストラクタがあると分かっても「後の祭り」です。 CImplのデストラクタが呼ばれることはありません。 これが先ほどの警告の意味です。

 解決策のひとつはCImplのデストラクタの有無を明示することです。 しかし、これはCImplの定義を明示することに他ならず、CImplは中身を見せたくないから前方参照になっているので、実装を隠蔽したい場合は解決策になりません。

 もうひとつの解決策は、クラスCのデストラクタを別のところで定義するです。 CImplはどこか開発者だけが見えるところにきちんとした宣言があるはずです。 じゃないとビルドできませんから。 その宣言が見えるところでC::~Cを定義するのです。 そして、これはCImplの実装と同様、利用する側からは見えないようにしておきます。 利用者が分かるのは「Cにデストラクタがある」ということと、「それはどこか他で定義されている」ということだけです。

#include <memory>

class CImpl;

class C {
  public:
    C();
    ~C();
  private:
    std::auto_ptr<CImpl> mpimpl;
};

void foo()
{
    C c;
}

 これだけで警告は収まるはず。 デストラクタがどこか他にある、と分かるので、オブジェクトcを破壊するときにファイル外の関数C::~Cを呼び出すようなコードが生成されます。 mpimplに対するデストラクタはそっちから呼び出されるので、関数fooではそれ以上のことを知る必要はありません。 ちなみにコンストラクタも必要でした(コンストラクタから例外が投げられたときの対策かな)。

$ g++ -c -W -O -Wall -S -masm=intel test.cpp
$ c++filt.exe < test.s
        .file   "test.cpp"
        .intel_syntax
        .text
        .align 2
.globl foo()
        .def    foo();  .scl    2;      .type   32;     .endef
foo():
        push    ebp
        mov     ebp, esp
        push    ebx
        sub     esp, 36
        lea     ebx, [ebp-24]
        mov     DWORD PTR [esp], ebx
        call    C::C()
L2:
        mov     DWORD PTR [esp], ebx
        call    C::~C()
        add     esp, 36
        pop     ebx
        pop     ebp
        ret
L1:
        .def    C::~C();        .scl    3;      .type   32;     .endef
        .def    C::C(); .scl    3;      .type   32;     .endef

 c++filtというコマンドはGCCの付属品で、C++のタイプセーフリンクのためにハナモゲラ化(普通はmanglingとか言います)されたシンボルを元に戻してくれます(demangling)。 例えば、C::~C()は本来は__ZN1CD1Evになっています。 分かるかこんなもん(笑)。 このようにフィルタにして使ってもいいですし、コマンドラインに直接シンボルを指定すれば、

$ c++filt __ZN1CD1Ev
C::~C()

と元に戻した名前を表示してくれます。 MSのコンパイラではundnameという(使い方は若干違いますが)同じような働きをするプログラムがついてきます。 覚えておくと便利。 アセンブラシンボルを見なければお世話になることもないでしょうけど。 いや、意外にDLLエクスポートされたシンボルを見るときにお世話になるかも。 manglingの方法は標準があるわけではなくコンパイラごとにマチマチなので、コンパイラ付属のツールを使う必要があります。

 コンストラクタとデストラクタはどこか他で定義してください。 ここでインラインで定義してしまうと、このファイルの中でインライン展開しようとしますから、結局元の木阿弥です。 面倒でも他のファイルで実体を定義してください。 たとえ空っぽだったとしても。 もちろん、実体を定義するファイルではCImplの定義が見える必要があります。

#include <memory>

class CImpl {
  public:
    CImpl();
    ~CImpl();
    ...
};

class C {
  public:
    C();
    ~C();
  private:
    std::auto_ptr<CImpl> mpimpl;
};

C::C() { }

C::~C() { }

 普通、クラスCの定義は利用者に提供するヘッダファイルに、CImplの定義は開発者だけが見るヘッダファイルに記述して、実装を記述するソースファイルでは両方をincludeします。 CImplの定義は「デストラクタの有無」が分かれば、デストラクタの定義を含んでいる必要はありません。

〜 〜 〜 〜 〜

 根本的には「前方参照宣言されているクラスのデストラクタを展開しようとすると問題が起きる」ということで、前方参照宣言だけされているクラスは実体を作れませんから、作るとすれば参照かポインタです。 newはサイズが分かっていないとできないので、問題になるのはほぼdeleteだけです。 実はテンプレートやデストラクタに限らず、前方参照宣言だけされているクラスへのポインタをdeleteしようとするとこの警告に出会うことになります。

class CImpl;

void foo(CImpl *p)
{
    delete p;
}

これだけでこの警告が出ます。

 逆にdeleteしなければポインタがあっても問題ないので、テンプレートで指定されたクラスのオブジェクトをどこか他からもらって、そのポインタを保持しているだけならば問題は起きません。

 ただ、前方参照宣言+ポインタ+deleteというパターンは実装隠蔽に使われることが圧倒的に多く、こんなものを何度も書くのは面倒なのでテンプレートにするのが普通です。 また、先ほどの例ならちょっと考えればすぐ分かると思いますが、たいていの場合はstd::auto_ptrを使うので、デストラクタの展開位置があいまいになりがちです。 そんな事情で、実用上は冒頭に書いたように

「テンプレート引数にクラスを指定する場合、引数に指定するクラスはデストラクタの有無が分かっていなければならない」

で「大体いいはず」です。


Copyright (C) 2011 You SUZUKI

$Id: templ-dtor.htm,v 1.8 2014/09/13 15:25:33 you Exp $