例外を投げられたときにプログラムがどんなふうに動作するか理解しておくことは重要でしょう。


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++を使ってコンパイルしても同じような結果が得られました。


なにはともあれ、生のリソースハンドル(ポインタなど)よりリソース管理オブジェクト(スマートポインタなど)を使った方が例外に対して安全であるということですね♪