C++での例外処理について

2005.4.3新規公開

C++には例外機構が備わっているが、C言語との互換性を確保するためなのか、不可解な動作をするところが多い。

例外を送出しない関数宣言

(この節は、2000.6.25の日記に加筆したもの。)

関数(またはメソッド)宣言でthrow ()を付けるとその関数からは例外を投げないという意味になる。にもかかわらず、例外を投げることができる。

次のソースは、関数f()で例外を投げる。gcc 3.4.2 (Fedora Core 3) では、コンパイル時にエラーも警告も出ない。

  3| #include <exception>
  4| #include <cstdio>
  5| 
  6| class E: public std::exception { };
  7| 
  8| void f() throw() { throw E(); }
  9| 
 10| int main() {
 11|     try {
 12|         f();
 13|     }
 14|     catch (E& e) {
 15|         printf("my exception thrown.\n");
 16|     }
 17|     catch (...) {
 18|         printf("something thrown.\n");
 19|     }
 20|     return 0;
 21| }

これを実行すると、例外が捕捉されず、プログラムが終了 (abort) する。

C++では、関数宣言にthrow(型, ...)を付けないとどのような例外も投げることができる。そのため、ある関数の内部でそのような関数を呼び出していると、呼び出すほうの関数もあらゆる例外を送出する可能性がある。

そんなわけで関数宣言に列挙していない例外を投げるプログラムを書いてもエラーにしないようになっているのでは、と思う。しかし陽に投げる例外と例外宣言がある関数の呼び出しぐらいはコンパイル時にチェックしてほしい。

実際の動作だが、関数宣言にない例外を送出しようとすると、std::unexpected()が内部で呼び出され、デフォルトの動作はstd::terminate()を呼ぶようになっている。そのため、プログラムがいきなり終了することになる。

std::set_unexpected()とstd::set_terminate()でハンドラを変更することができる。次のようにすると、いったんハンドラが呼び出されるようになる。

  1| #include <exception>
  2| #include <cstdio>
  3| 
  4| class E: public std::exception { };
  5| 
  6| void f() throw(std::bad_exception) { throw E(); }
  7| 
  8| void handler() {
  9|     printf("unexpected.\n");
 10|     throw std::bad_exception();
 11| }
 12| 
 13| int main() {
 14|     std::set_unexpected(handler);
 15| 
 16|     try {
 17|         f();
 18|     }
 19|     catch (E& e) {
 20|         printf("my exception thrown.\n");
 21|     }
 22|     catch (...) {
 23|         printf("something thrown.\n");
 24|     }
 25|     return 0;
 26| }

しかし実際問題としてこのハンドラを書くのは難しい。

というのも、ハンドラの内部では一体どこでどのような例外が発生したために自分が呼び出されたのかを知るすべがない。さらに、ハンドラ内で何か例外を投げないと、元の例外が再び送出されてabortしてしまう。かといって元の関数で許可されていない例外を投げるとstd::bad_exceptionを投げたものと見なされ、std::bad_exceptionも許可されていないとやっぱりabortしてしまう。

結局、何かログを取るくらいが関の山で、関数宣言でstd::bad_exceptionが許可されていることを期待してそれを投げるぐらいしかできない。元の関数に焦点を合わせると、列挙している例外以外が送出されることはない、ということになる。

コンストラクタで例外

コンストラクタで例外が発生すると、オブジェクト(インスタンス)の生成が完了せず、デストラクタも呼ばれない。

次のソースをコンパイルして実行すると、Dオブジェクトのデストラクタが呼ばれない。(この例ではabortするが、main()で例外を捕捉するようにしても同じ。)

  3| #include <cstdio>
  4| 
  5| class D { public: virtual ~D() { printf("D dtor\n"); }};
  6| 
  7| class C {
  8|     D* p;
  9| public:
 10|     C() { p = new D(); throw 1; }
 11|     virtual ~C() { delete p; p = NULL; }
 12| };
 13| 
 14| int main() {
 15|     C c;
 16|     return 0;
 17| }

コンストラクタでは例外が発生しないようにコーディングするか、例外が発生しても資源を適切に解放する(メソッドへの進入時点まで巻き戻す)ようにしなければならない。

外部リンク

Exception-Safety in Generic Components
コンポーネント、コンテナにおける例外安全性について
ゆーあいの参考HP
標準例外安全規則