その10 newしてvoid*化してdeleteするとはまる!
これは私がはまっていて掲示板にてご指摘頂いたことです。備忘録として残しておきます。
@ voidポインタのdeleteはデストラクタを呼ばない
voidポインタはどのようなポインタも代入できる汎用ポインタ変数です。この性質を利用して次のような図式になっている削除プログラムを作るとはまります:
int main() {
// オブジェクトを作成
MyClass* pObj = new MyClass();
// voidポインタにキャスト
void* Ptr = pObj;
// voidポインタを通して削除
delete Ptr;
}
上のプログラムは動きます。またnewで確保されたヒープ領域は解放されます。しかし、クラスのデストラクタが呼ばれません。
voidポインタは汎用ポインタですが、型の情報を失っているため、クラスのデストラクタを呼ぶ機構が働きません。これにより、例えばデストラクタで解放作業を行うつもりだったクラスは思わぬメモリリークを招く事になります。
この図式にはまったのは、newで確保したメモリを一括で削除するリストを作った時です。こんな感じのソースです:
class MyAry {
public:
MyAry(){};
virtual MyAry(){}
vector< int > m_ary; // 配列持ってます
};
void MyClass::create() {
m_pIntValue = new int;
m_list.push_back( m_pIntValue );
m_pAry = new MyAry;
m_list.push_back( m_pAry ); // これがやばい!
}
void MyClass::deleteList() {
list< void* >::iterator it = m_list.begin();
list< void* >::iterator end = m_list.end();
for ( ; it != end; it++ )
delete *it;
}
MyAryクラスにはvector<int>型の変数が定義されています。一方MyClass::createメソッドではMyClassが持っているメンバー変数をnewで確保しています。ここでm_pAryをm_listに登録しています。m_listはdeleteListメソッドで一括削除するためのvoid*型リストです。
これでdeleteListメソッドを呼ぶと、newで確保された領域は綺麗に削除されます。しかし、MyAryの型情報が失われているため、MyAryのデストラクタが呼ばれません。するとどうなるか?MyAryが持っているm_ary(vector)のデストラクタも呼ばれなくなってしまいます。vectorはデストラクタで自身が確保したメモリを解放しているので、ここでメモリリークが発生してしまいます。
voidポインタでマルチに削除しようという試みは、結果として思わぬバグを引き起こしたのでした(各方面すいません)。
A 削除担当人作成で回避
voidポインタにキャストするとまずい事がわかりました。でも、リストに様々なオブジェクトを登録して、後で一括deleteはしたい。そこで、削除を担当する人を作り問題を回避しました:
// 削除人基底
class DeleterBase {
public:
virtual ~DeleterBase() {}
};
// 特定のポインタ型を格納するクラス
template< class T >
class Deleter : public DeleterBase {
public:
T* pObj;
bool isAry;
Deleter( T* deletePtr, bool is_Ary ) : pObj(deletePtr), isAry( is_Ary ) {};
virtual ~Deleter(){
if ( isAry )
delete[] pObj;
else
delete pObj;
}
};
deleterBaseはデストラクタしかない変なクラスです。deleteはdeleterBaseクラスを継承しているテンプレートクラスです。中にはT型のポインタ変数(pObj)とそれが配列か否かのフラグがあるだけです。コンストラクタで削除するポインタとフラグを登録します。このクラスのオブジェクトが削除されるときに、デストラクタで保持しているポインタ先を消しに行きます。クラスは型情報を保持しているので、デストラクタもちゃんと呼ばれるようになります。
削除リストへの登録と削除作業は次のようになります:
int main() {
// 削除リスト
list< DeleterBase* > m_list;
// オブジェクトを作成し削除人に登録
Deleter<MyClass>* pdObj = new Deleter<MyClass>( new MyClass, false );
Deleter<OtherClass>* pdOther = new Deleter<OtherClass>( new OtherClass, false );
// リストに登録
m_list.push_back( pdObj );
m_list.push_back( pdOther );
// リストを削除
list< DeleterBase* >::iterator it = m_list.begin();
list< DeleterBase* >::iterator end = m_list.end();
for ( ; it != end; it++ ) {
delete *it;
}
}
親クラスであるDeleterBaseポインタのリストには、どのような型のDeleterも追加できます。削除時にはポインタをdeleteするだけです。
実は上の実装だけだとDeleterオブジェクトの実体を作ってコピーするとデストラクタでの削除でやはりはまるので、DeleterBaseに参照カウンタを付ける必要があるのですが、今はそれを省略してあります。
B 謝辞
バグのご指摘及び本章を書くきっかけを与えて下さいましたUMKさんとおだきゅ〜さんに感謝申し上げます。