g++ exception handling

Code Project という有名サイトに、VC++の例外処理方法に関する記事があります (http://www.codeproject.com/cpp/Exceptionhandler.asp) が、そこにg++の例外処理方法を解説したコメントがありました。


ざっくりと次のような事を言っています(翻訳してるわけではないので詳しくは英文読んでください)。

g++は、VC++とはちょっと違うやりかたで例外処理を実装している。g++の場合、実際に例外がthrowされない限りは、try/throw/catchを使ったコードを書いてもランタイムのコストはかからない。

foo()がbar()を呼んでいて、bar()が例外を投げるとせよ。このとき、foo()はスタックに戻りアドレス*1を置いてからbar()を呼ぶ。この戻りアドレスを仮にXとする。

このときコンパイラは、 (X, 掃除コードのアドレス) の組をテーブル化してオブジェクトコードに埋めておく。「掃除コード」もコンパイラが生成したもので、具体的には「bar()呼び出し中に例外がthrowされた場合に行わなければならない処理(foo()内でスタックに置いたオブジェクトのデストラクタ呼びなど)」が書かれる。こういうコードは g++ -S でアセンブリリストを吐かせれば目視で確認できる。

コンパイラが生成するテーブルは、throw処理*2が戻りアドレスをキーに高速に探索処理ができるよう、フォーマットが工夫されている。


まとめ:我々がg++用にコードを書く場合、留意しなければならないのは次です

  • コード中に try ... catch を書くだけでは実行速度は低下しない
  • コード中に throw を書くだけでは実行速度は低下しない
  • 実際に例外がthrowされた(throw文に制御が達した)ときだけランタイムコストがかかる
  • 例外を使うとオブジェクトサイズが肥大する。なぜなら、「例外を投げるかもしれない関数」を呼ぶ度に次が起こる為:
    • その関数が本当に例外を投げた場合に呼ばれる掃除コードが生成される
    • 例外送出コードから掃除コードを呼ぶためのテーブルが生成される

以上です。


x86+Linuxを仮定していることと、FC2付属のGCC(3.3.3)でしか仮説の確認をしていないことにご注意ください。例外処理方式が "SjLj" のg++を使うと結果が異なるかもしれません。

g++ exception handling (2)


C++のコーディングにおける良い習慣として次が知られています。

デストラクタには常に例外指定 throw() を書いておく

「デストラクタで例外を投げる」行為が例外なくダメダメであることや、その理由については、"Exceptional C++―47のクイズ形式によるプログラム問題と解法 (C++ in‐Depth Series)" のお陰で広く知れ渡っていると思いますので*3、無意識に上記のプラクティスを実行している人は多いのではないでしょうか。私もとりあえず次のように throw() と書いておくクチです。

class Foo {
public:
  ~Foo() throw() {
     // 解体処理
   }
};

さて、throw() 例外指定を行った場合、ランタイムコストやオブジェクトサイズへの影響はどうなるでしょう?


まず、最近のGCCを使っているのであれば、throw()指定を行ってもランタイムコストはかからない筈です*4。実際に g++ -S で throw() あるなしでの結果の違いを比べてみればわかりますが、throwを伴わない場合の正常系のコードに違いは見られない筈です。


次にオブジェクトサイズですが、これは非常に微妙です。結論から書くと、次のようになるようです。

  • throw()指定をした関数内で、例外を投げない関数*5しか呼んでいないならオブジェクトサイズは減る
  • throw()指定をした関数内で、例外を投げる可能性のある関数を呼んでしまっている場合、減らないかむしろ若干増える


デストラクタ以外に例外指定をすることはあまりないと考えて、ここでは話をデストラクタに限定します*6。次のようなコードを試してみます。

// 例外を投げるかもしれない関数
extern void may_throw(void);

// 例外を投げない関数
extern void no_throw(void) throw();

class A_throw {
public:
// 例外指定のないデストラクタ
    ~A_throw() {
        may_throw();
    }
};

class A_nothrow {
public:
// 例外指定があるが、デストラクタ内で例外が投げられる可能性のあるデストラクタ
    ~A_nothrow() throw() {
        may_throw();
    }
};

class A_really_nothrow {
public:
// 例外指定があり、デストラクタ内で例外が投げられる可能性のないデストラクタ
    ~A_really_nothrow() throw() {
        no_throw();
    }
};


// テストコード

void test1(void) {
    A_throw a1, a2, a3;
}

void test2(void) {
    A_nothrow a1, a2, a3;
}

void test3(void) {
    A_really_nothrow a1, a2, a3;
}


このようなコードを書くと、

  • test1()では、変数の個数に応じて「掃除コード」が生成される
  • test2()では、「掃除コード」は生成されないが、変数の個数に応じて「デストラクタ内で例外が発生してしまった場合にstd::unexpected()を呼ぶためのコード」が生成される
  • test3()では、余分なコードは生成されない

という結果になります。test1()のようなケースより、test2()のようなケースのほうが、生成されるコード量が多めなのが気になる点です。


まとめ:


デストラクタをthrow()指定すると、

  • 最近のGCCを使うなら、実行速度の低下はない
  • オブジェクトサイズの増減は微妙。大幅に減ることは期待できないカモ
    • 「実は例外を投げる可能性のあるデストラクタ」が多いと、むしろ増えるかも
  • でもまぁ、一律 throw() 指定でいいんじゃないかなぁ?
    • で、「本当に」例外を投げないようなデストラクタを書こう!


参考にしたURLは次です。

g++の例外処理方法は "SjLj*7 stack unwinding" と "dwarf2 stack unwinding" の二種類あるらしいですね。g++ -v すればわかるかも。


__cxa_throw() などの __cxa_XXX関数群は、gccのソースツリーの gcc-3.4.x/libstdc++-v3/libsupc++/ にある。多分。_Unwind_Resume() は、gcc-3.4.x/gcc/ かな。



追記(20040831): VC++の例外ハンドリングについてはRANTSさんのところに記事があります。

*1:foo()内のどこか

*2:__cxa_throw() かな?

*3:知らない人は読むと良いと思います

*4:2.9xなんかではthrow()指定すると微妙に実行が遅くなっていたような記憶がありますが

*5:やはりthrow()指定してある別の関数か、std::printfなどのCの関数

*6:フリー関数だと若干コンパイラの挙動が違う…

*7:setjmp-longjmp の略!