2010-08-03

例外指定

C++には、例外指定というものがある。これは、ある関数から投げられる例外を、あらかじめ明示的に指定しようという目的で作られた。

void f() throw(int, float, std::exception) ;

ここで、もし関数fが、int, float, std::exception以外の例外を投げた場合、関数std::unexpectedが呼ばれる。つまりこれは、関数tryブロックを使って以下のように書くのに等しい。

void f()
try {
// 関数本体
}
catch( int ) { throw ; }
catch( float ) { throw ; }
catch( std::exception ){ throw ; }
catch( ... ) { std::unexpected() ; }

この機能は、それほど難しいこともない。極論で言えば、上記のように、単なるソースコードの変形でも実装できるわけだ。この機能は、あまり実装するだけの価値もなかったので、多くの現実のコンパイラでは、単にパースだけして、無視された。

よくある誤解として、この機能は最適化のためにあるという思い込みがある。実際、この機能は最適化の役に立たないばかりか、むしろ最適化の妨げになる。というのも、いくつかの実装では、tryブロックというのは、存在するだけでコストがかかる代物だからだ。現実の多くのコンパイラは、実行時のパフォーマンスを犠牲にするよりは、規格を無視する方を選んだ。

ところで、この例外指定には、思わぬおまけがあった。throw()と指定した場合、その関数は、例外を投げないことを指定することになる。これは、規格的に考えると、例外を投げた場合、std::unexpectedを呼び出すことになる。

ところで、ここにMSVCという、規格を守らないことで非常に有名なコンパイラがある。MSVCの環境では、もし、ある関数が例外を投げないということが保証されていれば、行うことのできる、ある最適化手法が存在した。そこで、MSVCは、throw()が指定されていてもstd::unexpectedを呼び出さないばかりか、関数は例外を投げないという前提のもとに、この最適化を行ったのである。これはもちろん、規格違反である。

しかし、いくら理論的に優れていても、現実にそぐわない規格などというものには意味がない。C++0x制定にあたって、むしろ、この挙動を規格で定義しようではないかという動きが起こった。その結果、例外指定は大きく変わった。

まず、従来の、投げる可能性のある例外を指定する機能は、これまでと変わらない。

throw()という記述を、例外を投げないという意味を表すものと定義した。また、このthrow()と同じ意味を持つものとして、新しいキーワード、noexceptを導入した。throw()、noexcept、noexcept(true)は、無例外指定である。

void f() ; // 無指定、任意の例外を投げる可能性がある
void f() throw() ; // 無例外指定
void f() noexcept ; // 無例外指定
void f() noexcept(true) ; // 無例外指定
void f() noexcept(false) ; // 任意の例外を投げる可能性がある

もし、無例外指定の関数が例外を外に投げた場合、関数std::terminateが呼び出される。この挙動は、少し意外かもしれない。std::unexpectedを呼び出すという仕様が、オーバーヘッドの問題で無視されていたのに、これはどういうことか。

これは、現在のコンパイラの開発者の一致した意見として、例外を投げないと指定されている関数が例外を投げた場合、std::terminateを呼び出すことには、オーバーヘッドは存在しないからである。例外を投げないと指定されている関数が、例外を投げるというのは、そもそもバグである。セキュリティ上の観点から考えても、そのような場合、プログラムは強制終了するのが望ましい。

なお、無例外指定の関数が、例外を投げる場合、コンパイルエラーになるかどうかは、未規定である。というのも、すべての例をコンパイル時に検出するなどは、どだい無理だからだ。無例外指定の関数の内部で例外を使うのは、自由である。無例外の意味は、関数の外に例外を投げないということである。

// この関数は外に例外を投げる。
// コンパイルエラーになるべき
void f() noexcept
{
    throw 0 ;
}

// この関数は、絶対に外に例外を投げない
void f() noexcept
try {
    throw 0 ;
} catch( int ) {}

この例は、比較的簡単に思われる。では、以下の例はどうか。

void f() noexcept
try {
    throw 0 ;
}
catch( int )
{
    if ( rand()%2 ) throw ;
}

この場合、関数fが例外を投げるかどうかは、関数randの戻り値にかかっている。残念ながら、randの戻り値は、実行時にしか分からない。タイムマシーンを発明しない限り、コンパイル時エラーなどというものは、無理である。

このため、無例外指定が外に例外を投げる場合、コンパイルエラーになるという保証はない。もちろん、コンパイル時に判定できる場合、コンパイルエラーにしてもよい。規格はそのことを制限してはいない。

ちなみに、あるexpressionが例外を投げる可能性があるかどうかをコンパイル時に判定するのは可能である。noexcept演算子というものが、新しく定義されている。これについては、本の虫: noexcept operatorを参照。

4 comments:

Anonymous said...

> もちろん、コンパイル時に判定できる場合、コンパイルエラーにしてもよい。規格はそのことを制限してはいない。

n3092 では 15.4 p11 に以下の記述があります。
> An implementation shall not reject an expression merely because when
> executed it throws or might throw an exception that the containing
> function does not allow.
この記述に従うとコンパイルエラーにはできないと思うのですが、 n3092 より
後に何か変更があったりするんでしょうか?

江添亮 said...

その文章は、C++0x以前からあるもので、本来、dynamic-exception-specifications向けの文章なんですよね。
それに、その文章は、"merely because"となっていることから、それほど強い意味を持っていません。

Anonymous said...

> 本来、dynamic-exception-specifications向けの文章
この文面が dynamic ~ に限ると言える根拠は何ですか?

・・・これでしょうか?
http://www.open-std.org/jtc1/sc22/wg21/docs/cwg_active.html#1053
undefined behavior になればコンパイルエラーにもできますね。

> "merely because"となっていることから、それほど強い意味を持っていません。
???
規格の解釈に強弱なんて関係ありませんよね?

それに「"merely because"となっていること」で意味の強弱が変わるわけも
ないだろうと思います。

江添亮 said...

Pittsburgh会議の議事録を確認したのですが、このへんはだいぶ議論の対象になったようです。
元々がstd::terminateの呼び出しではなく、Undefined behaviorだったので。

どうやら、現在の文面だと、コンパイル時のエラーにするのは、規格に反するということになりそうです。