昔、比較的有名な某所で「コンストラクタからの例外送出」が「禁じ手」として紹介されていることもあり、コンストラクタから例外を送出すべきではないと考える人はいまだに少なくないようです。
その根拠となっているのは、コンストラクタから例外を送出した場合、デストラクタが呼ばれないためにリソースリークにつながるというものです。これは、次のようなケースを想定しているものと思われます。
0 1 2 3 4 5 |
foo::foo() : a(new A), b(new B) { } |
確かに、aまたはbのうち、後から初期化される側で例外が送出されると、他方が解放される機会が失われるため、リークにつながります。しかし、
0 1 2 3 4 5 6 7 |
void foo() { A* a = new A; B* b = new B; ... } |
でも同じことが起こるのではないでしょうか。つまり、リークが起きるのは、コンストラクタから例外が送出されたからではなく、例外安全ではない設計またはコーディングに問題があるからです。
オブジェクトの生存期間というのは、コンストラクタの処理が完了した時点から始まります。生まれていないものが(デストラクタによって)死ぬことはナンセンスですし、生まれていないものに対して、何らかの操作を行うのも同じくナンセンスです。
また、コンストラクタが失敗したことを通知する一般的な手段は例外しかありません。明示的にコンストラクタが呼ばれる場合は、エラーを格納するためのオブジェクトを、参照またはポインタで渡すこともできるでしょう。しかし、関数からのreturn時に呼び出されるコピーコンストラクタなど、それが不可能なケースもあるのです。
もし、マイコン向けの処理系のように例外処理をサポートしない場合には、コンストラクタが失敗したことをデータメンバに記録しておき、後からそれを参照する方法などで逃げなければならないかもしれません。しかし、それは非標準処理系における特例に過ぎません。
ところで、最初の例に挙げたように、aとbの両方のデータメンバ初期化時に例外が送出される可能性がある場合はどうすればよいのでしょうか?
リークを回避するには次のようにします。
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
foo::foo() : a(nullptr), b(nullptr) { try { a = new A; b = new B; ... } catch (...) { delete a; delete b; } } |
しかし、可能であれば、aとbをそれぞれnewするのではなく構造体にまとめるか、std::unique_ptrやstd::shared_ptrのようなスマートポインタを使用することで、もっと簡単に状況を改善する方がよいと思います。
参考までに、最初は「コンストラクタから例外を送出すべきではない」としていたにもかかわらず、後にその主張を撤回しているページをご紹介したいと思います。
結論としては、コンストラクタが失敗した場合には例外を送出することによってそれを通知すべきであり、それ以外の一般的な方法はありません。
最近ではRAII(Resource Acquisition Is Initialization)、日本語では「リソース取得は初期化」という考え方が一般的になり、この迷信も昔からC++を使い続けている方特有のものとなってしまったかもしれません。