C++話.delete this,すなわち「自殺するクラス」について.
delete thisという操作は,不正ではないが注意深く行う必要がある.
ごく単純なコードは次のようになる.
class A
{
public:
void suicide() { delete this; }
};
suicideは自分自身を破壊するpublicな非静的メンバ関数で,自らの意志で自
殺する際に他のメンバ関数から呼ばれる.あるいは,オブジェクト自身の手で
は自殺が行われない場合に,自殺を行わせるために外部から呼び出される.
suicide内でdelete thisが行われると,thisポインタが指すオブジェクトは破
壊され,記憶領域は開放される.したがって,以後は非静的データメンバへの
アクセスと仮想関数呼び出しを行ってはならない.どちらの操作もthisポイン
タが指す先を参照するからだ.また,非静的非仮想メンバ関数を呼び出すこと,
thisの値を評価することも控えた方が懸命である: これらの操作は通常は致命
的ではないが,意味がないし,その後に致命的操作を伴う可能性が高い.
ここでは,suicideを正しく実装できたとして,それをなるべく安全に用いる
ようにする方法について扱う.
(1)のコードは,かなり注意深く使用する必要がある.例えば次のように用い
ると,コンパイルには成功するが実行時エラーが発生する.
{
A a; // 構築
a.suicide(); // 自殺による破壊
} // 実行時エラー: 2度目の破壊
オブジェクトaはsuicideにより破壊され,ブロック終了箇所でもう一度破壊さ
れる.2度目の破壊で実行時エラーが発生する.
見方を変えれば,suicideは「独自の方法でオブジェクトを破壊する関数」で
ある.このような標準的方法とは相容れないオブジェクトの構築あるいは破壊
を明示的に行う関数を設けたということは,オブジェクトのライフタイム管理
をプログラマの責任で行うようにした,ということを意味する.したがって,
グローバルオブジェクトやスタック上のローカルオブジェクトといった処理系
がライフタイムを管理するオブジェクトの存在を禁止するようコードに明示し,
不正をコンパイルエラーとして捕捉できるようにするべきである.
これらを禁止する方法として,コンストラクタすべてをprivateにする方法と,
デストラクタをprivateにする方法がある.後者の方法は次の通り.
class A
{
public:
void suicide() { delete this; }
private:
~A() {}
};
これで,処理系あるいは他のクラスがライフタイムを管理するオブジェクトは
記述できなくなった.
A a1; // コンパイルエラー: ~A()を呼び出せない
foo()
{
A a2; // コンパイルエラー: ~A()を呼び出せない
std::auto_ptr<A> p1(new A); // コンパイルエラー: ~A()を呼び出せない
A* p2(new A); // OK
p2->suicide(); // OK
}
しかし,まだ抜け道がある.次のplacement newを用いたコードは,一見正当
に見えるが実行時エラーが発生する.
foo()
{
void* c(operator new(sizeof(A))); // 領域を動的に確保
A* p(new(c) A); // 領域上にオブジェクトを構築
p->suicide(); // 自殺
operator delete(c); // 領域開放: 実行時エラーが発生する
}
bar()
{
char c[sizeof(A)]; // 領域を静的に確保
A* p(new(c) A); // 領域上にオブジェクトを構築
p->suicide(); // 自殺: 実行時エラーが発生する
}
(2-b)のfooは operator delete(c) で実行時エラーが発生する.なぜなら,
p->suicide() がオブジェクトの破壊と領域開放の両方を行ってしまっており,
領域cは開放済みであるからだ.また,barは p->suicide() で実行時エラーが
発生する.なぜなら,suicideはオブジェクトの破壊と領域開放の両方を行お
うとするが,cは静的であり領域開放はできないからだ.
通常のnewを許可しつつplacement newだけを禁止する方法は,存在するが煩雑
である.それよりも,すべてのコンストラクタをprivateとすることですべて
のnewを禁止し,代わりに独自のオブジェクト構築関数を静的メンバ関数とし
て用意する方法の方がわかりやすい.
class A
{
public:
static A* create() { return new A; }
void suicide() { delete this; }
private:
A() {}
A(const A&) {}
~A() {}
};
foo()
{
A* p(A::create()); // OK
p->suicide(); // OK
}
結局のところ,独自の破壊関数の使用を強制するならば,対応する独自の構築
関数を用意してその使用を強制するべきである,という結論に落ち着くようで
ある.
なお,このような標準から逸脱する構築や破壊の使用を強制することは好まし
いものではないので,必須の文脈でのみ用いるようにした方がよい.例えば,
(2-a)に示したようにこのクラスはstd::auto_ptrのような所有権管理ツールと
の併用が難しく,例外安全にするべき文脈で扱いにくいといった問題がある.
(3)のクラスを公開派生に対応させるには次のようにする.
class A
{
public:
static A* create() { return new A; }
void suicide() { delete this; }
protected:
A() {}
A(const A&) {}
virtual ~A() {}
private:
A& operator=(const A&);
};
class B : public A
{
public:
static B* create() { return new B; }
private:
B() : A() {}
B(const B& v) : A(v) {}
virtual ~B() {}
};
foo()
{
A* pa(A::create()); // OK
pa->suicide(); // OK
A* pb(B::create()); // OK
pb->suicide(); // OK
}
公開派生を許容するクラスAでは,まずデストラクタを仮想にする.Aのすべて
のコンストラクタとデストラクタを,外部からの呼び出しを禁止しつつBから
呼び出せるようにするためにprotectedにする.派生クラスのオブジェクトが
混在しうる代入はスライスが発生するため,privateとして宣言し定義を与え
ないことで禁止する.suicide内のdelete thisは仮想デストラクタにより仮想
的に機能するので,suicideの仮想化は不要である.
派生クラスBでは,これまでの議論の通りすべてのコンストラクタとデストラ
クタをprivateにする.B用のオブジェクト構築関数createを定義し,
A::createを隠蔽する.破壊関数suicideはA::suicideをそのまま用いる.
見ての通り,Aは派生クラスBに対しcreate/suicideインタフェースを完全には
強制できておらず,Bで問題のあるコーディングが行われることを防げない.
こういった意味でAは不完全である: 構築と破壊は単純には継承されないので
無理もない話だが,不完全であることを認識した上で限定的に使用するべきだ
ろう.
ここまで書いておいてなんだが,suicideをprivateにしてクラス内からだけ呼
び出せるようにし,クラス外部に対してsuicideの使用を強制しないようにす
れば,上記のような苦労はしないで済む(構築と破壊を注意深く行わなければ
ならないことに変わりはないが).あるいは,自らを破壊する行為をやめて,
破壊してほしい旨を所有者に依頼するような構成にする方法もあるだろう.そ
の方がずっと優れているかもしれない.