例外を投げられたときにプログラムがどんなふうに動作するか理解しておくことは重要でしょう。
operator new をオーバーロードすることで簡単にbad_allocを投げるクラスを定義できます♪
コンストラクタの中で例外が投げられたときどんな動作をするのか?
すでに new に成功しているオブジェクトはdeleteされるのか?
確認するためのちょっとしたサンプルプログラムを作ってみました。
#include <iostream>
#include <new>
using namespace std;
class Widget
{
public:
Widget()
{
cout << __FUNCTION__ << endl;
}
~Widget()
{
cout << __FUNCTION__ << endl;
}
void* operator new (size_t size)
{
static char chunk[sizeof(Widget)];
static bool was_called = false;
if (was_called) {
throw bad_alloc();
}
was_called = true;
cout << "Widget::" << __FUNCTION__
<< ": p = " << static_cast<void*>(chunk) << endl;
return chunk;
}
void operator delete(void* p)
{
cout << "Widget::" << __FUNCTION__ << ": p = " << p << endl;
}
};
class SomeObject
{
public:
SomeObject()
: widget_1(new Widget)
, widget_2(new Widget)
{
cout << __FUNCTION__ << endl;
}
~SomeObject()
{
cout << __FUNCTION__ << endl;
delete widget_2;
delete widget_1;
}
void* operator new (size_t size)
{
void* p = ::operator new(size);
cout << "SomeObject::" << __FUNCTION__ << ": p = " << p << endl;
return p;
}
void operator delete(void* p)
{
cout << "SomeObject::" << __FUNCTION__ << ": p = " << p << endl;
::operator delete(p);
}
private:
Widget* widget_1;
Widget* widget_2;
};
int main()
{
try
{
SomeObject* someObject = new SomeObject;
}
catch (exception& e) ← 参照でキャッチしましょう
{
cout << e.what() << endl;
}
return 0;
}
Widget クラスにはコンストラクタ・デストラクタ・new演算子・delete演算子にそれぞれ動作ログを仕込んであります。そして、new演算子は2回目以降、呼ばれるとstd::bad_allocを投げます。
ちなみに、__FUNCTION__を使うと動作している関数の名前がわかります。ただし、__FILE__や__LINE__と違って標準ではないので、コンパイラによっては使えません(gccとVC++では使えます)。
SomeObject クラスはコンストラクタでWidgetクラスを2つ生成しています。したがって、2つ目のWidgetが生成されたときに例外が投げられます。
これをコンパイルして動作させるとこんなログが出力されました。
SomeObject::operator new: p = 0x9874008
Widget::operator new: p = 0x804a484
Widget
SomeObject::operator delete: p = 0x9874008
St9bad_alloc
最初にSomeObjectのnew演算子が実行され、次にWidgetクラスの1回目のnew演算子の実行があり、次いでWidgetのコンストラクタが実行され、SomeObjectのdelete演算子が実行され、最後にe.what()のログが出力されています。2回目のWidgetクラスのnew演算子でbad_allocを投げているのでWidgetクラスのnew演算子とコンストラクタのログは1回ずつしか出力されていません。しかし…
なんと、new に成功したWidgetのデストラクタもdelete演算子も実行されていません!!
これではメモリリークを起こしてしまいます。
しかし、SomeObject クラスの実装を次のように変更するとどうなるでしょうか?
class SomeObject
{
public:
...
~SomeObject()
{
cout << __FUNCTION__ << endl;
delete widget_2;
delete widget_1;
}
...
private:
Widget* widget_1;
Widget* widget_2;
auto_ptr<Widget> widget_1; ← std::tr1かboostのshared_ptrでもよい
auto_ptr<Widget> widget_2;
};
動作ログは次のように変わります。
SomeObject::operator new: p = 0x8ba6008
Widget::operator new: p = 0x804a5a0
Widget
~Widget
Widget::operator delete: p = 0x804a5a0
SomeObject::operator delete: p = 0x8ba6008
St9bad_alloc
今度はちゃんとWidget クラスのデストラクタとdelete演算子が呼ばれています♪
SomeObjectのメンバ変数をWidget*からスマートポインタ(auto_ptrやshared_ptr)に変えるだけで、なぜこんなに動作が違うのか??
正直、う~にもよくわかりません(勉強不足ですみません)。
う~もそこまでC++言語仕様に詳しくないのです…
なお、上のログはLinux上でgccを使ってコンパイルし動作させたときのログですが、WindowsでVC++を使ってコンパイルしても同じような結果が得られました。
なにはともあれ、生のリソースハンドル(ポインタなど)よりリソース管理オブジェクト(スマートポインタなど)を使った方が例外に対して安全であるということですね♪