規格で学ぶ、例外と未定義の動作
例外周りを今まできちんとやってこなかったので、ちょいちょい調べたところ、日本語のWEB上の情報だけでは足りなかったので、規格にあたった。
戻り値を戻さない制御
以下のプログラムは多くのコンパイラで警告を発する。
int f(){} int main(){}
理由は関数f
が戻り値を戻さないからだ。戻り値の型がvoid
の場合は問題ないが、戻り値を伴う関数戻り値を戻さかった時、その動作はundefined behavior
となる。main
関数においては、return 0;
を意味する。
6.6.3 The return statement 2 ... Flowing off the end of a function is equivalent to a return with no value; this results in undefined behavior in a value-returning function. ...
2 ... 関数の末尾に到達する事は、値を伴わない
return
と等価である。これは値を戻す関数においては、未定義の動作を引き起こす。 ...
3.6.1.5
A return statement in main has the effect of leaving the main function (destroying any objects with automatic storage duration) and calling std::exit with the return value as the argument. If control reaches the end of main without encountering a return statement, the effect is that of executingreturn 0;
main
内のreturn
文はmain
関数からの離脱を意味し(自動変数の寿命を持つあらゆるオブジェクトを破棄する)、std::exit
を戻り値を引数として呼ぶ。もし制御がreturn
文に遭遇する事無くmain
の末尾に到達した場合、return 0;
が実行される。
尚、clangやgccでは警告は出るがビルドは通る。文法上はwell-formed
だからだ。VC++では強い警告時はデフォルトではビルドが通らないので、ビルドエラーになる。
int f(){ throw 0; } int main(){ f(); }
簡単のため、送出する例外の型はint
にしている。上記のコードでは、例外がcatch
されず関数f
を離脱する。
15.1 Throwing an exception ... 2 When an exception is thrown, control is transferred to the nearest handler with a matching type (15.3); “nearest” means the handler for which the compound-statement or ctor-initializer following the try keyword was most recently entered by the thread of control and not yet exited.
2 例外が創出された時、制御は最も近い適合する型のハンドラに転送される。「最も近い」とは、最も最近スレッド制御が突入し、まだ終了していない
try
に続く複合文またはメンバイニシャライザを意味します。 15.3 Handling an exception ... 9 If no matching handler is found, the function std::terminate() is called; whether or not the stack is unwound before this call to std::terminate() is implementation-defined (15.5.1).
もし適合するハンドラが見つからなかった場合、
std::terminate
が呼ばれる。スタックがstd::terminate
への呼び出し前に巻き戻されているか否かは実装定義である。
main
関数の末尾まで制御が転送され、std::terminate
が呼ばれる。スタックが~云々の件はよくわからない。教えて強い人。
try-block
前述の規格に基づく例外が送出された際の制御の転送の例。
int f() { try{ throw 0; } catch( int e ){ /* 最も近いハンドラでキャッチされる */ } } int main() { try{ f(); } catch( int e ){ /* ここではキャッチされない */ } }
int f(){ throw 0; } int main() { try{ f(); } catch( int e ){ /* ここでキャッチされる */ } }
int f(){ throw 0; } int main() { try{ f(); } catch( char e ){ /* 型がマッチされないのでキャッチされない */ } } // ここで std::terminate が呼ばれる
function-try-block
以下のような型について考える。
struct inner { inner(){ throw 0; } } struct outer { inner m_inner; outer(){ /* この時点で m_inner のインスタンス化は終了している */ } }
outer
をインスタンス化した時、メンバ変数m_inner
のインスタンス化の際に例外が送出される。この例外をouter::outer
でキャッチしたいが、通常のtry-catch-block
ではキャッチ出来ない。メンバ変数のコンストラクタの呼び出しはコンストラクタの内部に入った時点で終了しているからだ。
そこで、C++にはfunction-try-block
と呼ばれる構文が用意されている。
struct outer { inner m_inner; outer()try{} catch( int e ){ /* ここで例外をキャッチする。ついでに再送出する。 */ } }
コンストラクタやデストラクタでfunction-try-block
を用いて例外をキャッチした時、そのままハンドラの末尾に到達した場合、その例外は再送出される。
15 The currently handled exception is rethrown if control reaches the end of a handler of the function-try-block of a constructor or destructor. Otherwise, a function returns when control reaches the end of a handler for the function-try-block (6.6.3). Flowing off the end of a function-try-block is equivalent to a return with no value; this results in undefined behavior in a value-returning function (6.6.3).
15 もしコンストラクタまたはデストラクタの
function-try-block
内のハンドラの末尾に制御が到達した場合、ハンドル中の例外が再送出される。それ以外の場合、制御がfunction-try-block
内のハンドラの末尾に到達した時、関数はreturn
する。function-try-block
の末尾に到達する事は、値を伴わないreturn
と等価であり、これは値を戻す関数においては未定義の動作を引き起こす。
末尾に到達しなかった場合、例外は再送出されない。以下のように記述した場合だ。
struct outer { outer()try{} catch( int e){ throw 1; } }
末尾に到達する前に別の例外を送出しているので、キャッチした例外は再送出されない。尚、コンストラクタのfunction-try-block
内のハンドラ内ででreturn
文を記述する事はill-formed
である。
14 If a return statement appears in a handler of the function-try-block of a constructor, the program is ill-formed.
デストラクタではどうなのかは良くわからない。そもそもデストラクタ内で例外を送出すること自体が推奨されない(ここでは言及しないのでぐぐられたし)。
function-try-block
はコンストラクタやデストラクタだけでなく、通常の関数でも使用できる。
void f()try{ throw 0; } catch( int e ){} int main(){ f(); }
コンストラクタやデストラクタでないので、例外は再送出されない。その代わり、値を伴わないreturn;
を意味する。つまり、次のコードと等価である。
void f()try{ throw 0; } catch( int e ){ return; } int main(){ f(); }
コンストラクタではないので、ハンドラ内でreturn
してもよい。
値を戻す関数の場合は以下のように記述する。
int f()try{ throw 0; } catch( int e ){ return 1; } int main(){ f(); }
上記のコードでは例外は関数f
の内部で処理され、f
は1
を戻す。main
内に例外は波及しない。ただし、以下のコードは未定義の動作を引き起こす。
int f()try{ throw 0; } catch( int e ){} int main(){ f(); }
int
型の戻り値を戻さなければならないのに戻していないからである。
ところで
Flowing off the end of a function-try-block is equivalent to a return with no value; this results in undefined behavior in a value-returning function (6.6.3).
上記の記述は"Flowing off the end of a handler of function-try-block"じゃないの?と思うのだがどうなのだろうか。教えて強い人。
他にも
等の問題があるが、規格書を全部翻訳する余裕は無いので自分で読んでほしい。