「C++のコンストラクタで例外を投げてはいけない」は迷信

via. http://www.kmonos.net/wlog/83.html#_1255080321

この件に関してはずっと誤解していたので、調べてみた。
昨日の#lowhacksで騒いでいた件のまとめ。

「C-torで例外を投げてはいけない」というのは、確かに迷信のようだ。


むしろ、Exceptional C++の著者Herb Sutter先生C++ FAQ Liteでは「C-torの失敗」を伝える方法として積極的に使うよう推奨されている。

「C-torが例外を投げてはいけない」の迷信の根拠として、良く言われているのがC-torが失敗すると、D-torが呼ばれないので、リソースリークが起こる、ということだ。

これは正しくて、C-torが失敗した場合、その生成に失敗したクラスのD-torは呼ばれない。

ためしに以下のようなコードを実行すると、

#include <iostream>

void* operator new (size_t size)
{
	void* p = ::malloc(size);
	std::cout << "new size: " << size << ". p = " << p << std::endl;
	return p;
}

void operator delete (void* p) {
	std::cout << "delete " << p << std::endl;
	::free(p);
}

class Res {
public:
	Res() { std::cout << "Res C-tor" << std::endl;	}
	~Res() { std::cout << "Res D-tor" << std::endl;	}
};

class Member {
public:
	Member() { std::cout << "Member C-tor" << std::endl; }
	~Member() { std::cout << "Member D-tor" << std::endl; }
};

class A {
public:
	A() {
		std::cout << "A C-tor" << std::endl;
		m_pres = new Res;

		throw 1;
	}

	~A() {
		delete m_pres;
		std::cout << "A D-tor" << std::endl;
	}

private:
	Res* m_pres;
	Member m_member;
};

int main() {
	try {
		A* pa = new A;

		std::cout << "doesnot come here!" << std::endl;

		delete pa;
	}
	catch(int e) {
		std::cout << "exception : " << e << " caught" << std::endl;
	}

	return 0;
}

実行結果はこのようになる。

new size: 8. p = 003A5E40
Member C-tor
A C-tor
new size: 1. p = 003A5F20
Res C-tor
Member D-tor
delete 003A5E40
exception : 1 caught

つまり、このコードは以下のような手順で実行されていることがわかる。

  1. Aメモリ確保
  2. メンバ変数のC-tor
  3. AのC-tor
    1. Resメモリ確保
    2. ResのC-tor
  4. メンバ変数のD-tor
  5. Aメモリ開放

確かに、Aのデストラクタは実行されていない。
AのC-torで確保したm_presはコンストラクタでdeleteされることなく、そのままリークしてしまう。

注:A自身のメモリはリークしていないことに注意。C-torがthrowした場合、自動的にdeleteは呼び出される。delete this;を書かなければならないというのは迷信。

しかし、メンバ変数自身のD-torはきちんと呼び出されていることがわかる。

よって、メンバ変数それぞれに各リソースの責任を持たせスマートポインタ等を用いRAIIパターンを徹底すれば、AのD-torが呼び出されなくても正しくリソースを開放するように書くことができる。

先ほどの例では、Aクラスの実装をスマートポインタstd::auto_ptrを使うように書き換えれば、正しくリソースを開放することができる。

class A {
public:
	A() {
		std::cout << "A C-tor" << std::endl;
		m_pres.reset(new Res);

		throw 1;
	}

	~A() {
		std::cout << "A D-tor" << std::endl;
	}

private:
	std::auto_ptr<Res> m_pres;
	Member m_member;
};

実行結果:

new size: 8. p = 003E6088
Member C-tor
A C-tor
new size: 1. p = 003E6168
Res C-tor
Member D-tor
Res D-tor
delete 003E6168
delete 003E6088
exception : 1 caught

作業中。まだ続くよ!

回線が不安定なので続きはあとで