(2005.4.3) 新規公開
(2017.6.18) 最近の言語仕様に更新. 大幅に加筆。
C++には例外機構が備わっているが、C言語との互換性を確保するためなのか、不可解な動作をするところが多い。
(この節は、2000.6.25の日記に加筆したもの。)
関数(またはメソッド)宣言で throw ()
を付けると, その関数からは例外を投げないという意味になる。にもかかわらず、中で, 例外を投げることができる。
次のソースコードは、関数 f()
のなかで例外を投げる。gcc 3.4.2 (Fedora Core 3) では、コンパイル時にエラーも警告も出ない。
[2017-06] gcc 6.3.1 (Fedora 25 Linux) では, f() throw()
では警告が出ないが, f() noexcept
で、かつブロック内に直接 throw
文があるときには警告が出る。
これを実行すると、例外が捕捉されず、プログラムが終了 (abort) する。
$ ./a.out terminate called after throwing an instance of 'E' what(): my exception Aborted (コアダンプ)
C++では、関数宣言に throw (型, ...)
(動的例外指定という。) または noexcept
を付けないと, どのような例外も投げることができる。ある関数が, 潜在的に例外を投げうる (potentially-throwing) 関数を呼び出していると、その呼び出すほうの関数もあらゆる例外を送出する可能性がある。
逆に, 例外を投げるコードがあっても, 実際に例外を投げうるのかをコンパイル時に確実に判定することは難しい。そのため, 動的例外指定に列挙していない例外を投げるような字面でも, コンパイルエラーにはならない。上の例のように, 例外を投げるコードの有無で, 警告を出すのがせいぜい。
実際の動作だが、動的例外指定にない例外を送出しようとすると、実行時に, std::unexpected()
が内部で呼び出される。デフォルトの動作は std::terminate()
を呼ぶようになっている。noexcept
に反した場合は, いきなり std::terminate()
が呼び出される。
std::terminate()
は, プログラムを abort させる。結局, 終了することになる。
一応, std::set_unexpected()
とstd::set_terminate()
でハンドラを変更することができる。次のようにすると、自分のハンドラが呼び出されるようになる。
unexpectedハンドラは、次のように動く;
throw(...)
に違反なので, std::terminate()
に進む
throw(...)
に列挙されている型なら,
最初からそれが投げられたかのように、例外が取り回わされる
throw(...)
に列挙されていない場合, std::bad_exception
にさらに差し替わる.
その上で, bad_exception
が列挙されていない場合, やっぱり terminate()
.
しかし実際問題, このハンドラで何か意味のある処理をさせるのはできない。
unexpected
ハンドラの内部では, 一体どこでどのような例外が発生したために自分が呼び出されたのかを知るすべがない。さらに、ハンドラ内で何か例外を投げないと、元の例外が再び送出されてabortしてしまう。かといって元の関数で許可されていない例外を投げると std::bad_exception
を投げたものと見なされ、std::bad_exception
も許可されていないと, やっぱりabortしてしまう。
結局、何かログを取るくらいが関の山で、関数宣言で std::bad_exception
も指定されていることを期待してそれを投げるぐらいしかできない。
元の関数に焦点を合わせると、やはり, 列挙している型以外の例外が送出されることはありえない、ということになる。
エラー処理の方法はいろいろ考えられる。
方法 | 難点 |
---|---|
戻り値によってエラーかどうか (-1とかナル値とか), あるいはエラーコードを返す。 | エラーコード以上のことが分からない. |
オブジェクトの状態を変化させ, 別のメソッドで問い合わせる. | エラー処理の書き忘れ |
Eitherを返す | |
例外 | |
モナド |
例外によってエラー処理をおこなう場合, 例外安全性を満たす必要がある。
例外安全性は,次の3つの水準がある。
上位レイヤーが関連リソースにアクセスできない場合、関連リソースを解放する責任がある。この水準は、常に満たす必要がある。
整合性さえ取れていればいいので, 例えば, 10個の要素をコンテナに追加するメソッドの中で例外が発生したとき、コンテナにまったく追加されていないかもしれないし、5個追加されているかもしれない。
C++での実装: RAIIイディオム (Resource Acquisition Is Initialization) などを活用。
上の例では、必ず、コンテナに何も挿入されていない状態にならなければなりません。
IOなど、原理的に「強い保証」を満たせない状況もある。出力済みのものは巻き戻せない。
注意したいのは、単に例外を発生させなければいい、というわけではない。例えば, C++ の ofstream
はファイルを開くのに失敗しても例外を発生しない。例外は発生しないが, 処理としては no-fail ではない。
可能な場合は「強い保証」を満たすようにするのが望ましいが、そんなに簡単ではない。
より下のレイヤーから上がってきた例外への対処。※ネット上で「例外回復」という用語を見かけたが、そういう言い方はしないように思う。
単に、次を満たすようにすればいい。
例外に当てはめると, 自分で対処できる例外のみ catch
し、処理する。握りつぶす (再送しない)。対処・解決できない例外は, そのままか, 再送によって上位レイヤーに伝えるようにする。これは「例外中立」と呼ばれる。
catch
で何でもかんでも捕捉しないようにするのが肝要。型を特定し、内容を確認する。
catch
するか、catch
して再送するか、突き抜けるか, の3択。プログラミング言語によって finally
があったりなかったり。C++にはない。C#にはある。
Ruby には例外が発生したbegin
節の最初からやり直す命令 (retry
) があるが、珍しい。継続と, 継続を取り出す機能 call-with-current-continuation (call/cc
) が必要。
swap
関数C++11 での __cplusplus
の値は 201103L. 余談。
C++は、変数がオブジェクトの領域を確保する。swap
ですら例外を投げかねないという, 特有の難しさがある。
noexceptとなる条件について。
std::swap
関数は, 型Tがムーブ構築可能かつムーブ代入可能な場合のみ, 定義される。<utility> ヘッダ (C++11以降) または <algorithm>ヘッダ.
gcc 6.3.1 (libstdc++) の実際の定義は, 次のようになっている。
is_move_constructible<T>
構造体は, 型Tがムーブ構築可能かどうか. is_move_assignable<T>
構造体は, 型Tがムーブ代入可能か.
C++では, コンストラクタで例外が発生すると、オブジェクト(インスタンス)の生成が完了せず、デストラクタも呼ばれない。
次のソースをコンパイルして実行すると、C
クラスのデストラクタが呼ばれない。この例ではabortするが、main()で例外を捕捉するようにしても同じ。
実行結果.
$ ./a.out terminate called after throwing an instance of 'int' Aborted (コアダンプ)
コンストラクタでは例外が発生しないようにコーディングするか、例外が発生しても資源を適切に解放する(メソッドへの進入時点まで巻き戻す)ようにしなければならない。