altebute.hatenablog.com

犬も歩けば規格にあたる

規格で学ぶ、例外と未定義の動作

例外周りを今まできちんとやってこなかったので、ちょいちょい調べたところ、日本語の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 executing

return 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の内部で処理され、f1を戻す。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"じゃないの?と思うのだがどうなのだろうか。教えて強い人。

他にも

  • 静的グローバル変数や、任意のクラスの静的メンバ変数のコンストラクタやデストラクタが例外を送出するとどうなるのか
  • ある構造体やクラスのメンバ変数のデストラクタが例外を送出した時どうキャッチするのか

等の問題があるが、規格書を全部翻訳する余裕は無いので自分で読んでほしい。