back

C++の例外処理とうまく付き合う

スタック巻き戻しコード

例外指定

スタックの巻き戻しとデストラクタ

例外が起きる可能性のある関数

try ... catch

例外と標準クラス

例外を投げないはずの関数が例外を投げる

-fno-exceptions

例外指定と仮想関数

まとめ

その他

top

スタック巻き戻しコード

 g++でプログラムを組んでいて、意外に実行速度の邪魔になるのが例外処理のスタック巻き戻しコード。 スタック巻き戻しコードとは、ある関数が他の関数を呼び出し、呼び出し先で例外が発生した場合に、呼び出し元が実行すべきデストラクタを津々浦々、粛々と呼び出すためのコード。 通常、関数に入ったところでデストラクタ呼び出し部分を登録して、関数を出るところで登録を抹消する。 例えばこんなコード。

class C {
  public:
    ~C();
};

void bar();

void foo()
{
    C c;
    bar();
}

g++ -O2 -S とすると、こんな長いコードが生成される。ちなみにCygwin付属のgcc 3.4.4にてコンパイル。

__Z3foov:
    pushl   %ebp
    movl    %esp, %ebp
    leal    -24(%ebp), %eax
    subl    $120, %esp
    movl    %eax, -60(%ebp)
    leal    -92(%ebp), %eax
    movl    %ebx, -12(%ebp)
    movl    %esi, -8(%ebp)
    movl    %edi, -4(%ebp)
    movl    $___gxx_personality_sj0, -68(%ebp)
    movl    $LLSDA2, -64(%ebp)
    movl    $L6, -56(%ebp)
    movl    %esp, -52(%ebp)
    movl    %eax, (%esp)
    call    __Unwind_SjLj_Register
    movl    $1, -88(%ebp)
    call    __Z3barv
L1:
    movl    $-1, -88(%ebp)
    leal    -40(%ebp), %eax
    movl    %eax, (%esp)
    call    __ZN1CD1Ev
    leal    -92(%ebp), %eax
    movl    %eax, (%esp)
    call    __Unwind_SjLj_Unregister
    movl    -12(%ebp), %ebx
    movl    -8(%ebp), %esi
    movl    -4(%ebp), %edi
    movl    %ebp, %esp
    popl    %ebp
    ret
    .p2align 4,,7
L6:
L2:
L4:
    addl    $24, %ebp
    movl    $0, -88(%ebp)
    movl    -84(%ebp), %eax
    movl    %eax, -96(%ebp)
    leal    -40(%ebp), %eax
    movl    %eax, (%esp)
    call    __ZN1CD1Ev
    movl    $-1, -88(%ebp)
    movl    -96(%ebp), %eax
    movl    %eax, (%esp)
    call    __Unwind_SjLj_Resume
top

例外指定

ある関数で発生するかもしれない例外は、例外指定として書いておくことができる。 これを使ってbarが例外を出さないと明示してやれば、

class C {
  public:
    ~C();
};

void bar() throw();
void foo()
{
    C c;
    bar();
}

こんなに短いコードになる。

__Z3foov:
L2:
    pushl   %ebp
    movl    %esp, %ebp
    subl    $40, %esp
    call    __Z3barv
    leal    -24(%ebp), %eax
    movl    %eax, (%esp)
    call    __ZN1CD1Ev
    leave
    ret
top

スタックの巻き戻しとデストラクタ

 スタックの巻き戻し処理は、デストラクタを呼び出すためのものだから、barが例外を出すかもしれない場合でも、デストラクタさえなければ、

class C {
  public:
    C();    // コンストラクタはあるが、デストラクタはない
};

void bar();

void foo()
{
    C c;
    bar();
}

やっぱり短く済む。

__Z3foov:
    pushl   %ebp
    movl    %esp, %ebp
    leal    -1(%ebp), %eax
    subl    $8, %esp
    movl    %eax, (%esp)
    call    __ZN1CC1Ev
    call    __Z3barv
    leave
    ret

 でも、基底クラスにデストラクタがあれば、それを呼ばないといけないから、

class C {
  public:
    ~C();
};

class D : public C {
    // デストラクタがないように見えるけど、
    // C::~C() は呼ばなければならない。
};

void bar();

void foo()
{
    C c;
    bar();
}

長くなっちゃう。

__Z3foov:
    pushl   %ebp
    movl    %esp, %ebp
    leal    -24(%ebp), %eax
    subl    $120, %esp
    movl    %eax, -60(%ebp)
    leal    -92(%ebp), %eax
    movl    %ebx, -12(%ebp)
    movl    %esi, -8(%ebp)
    movl    %edi, -4(%ebp)
    movl    $___gxx_personality_sj0, -68(%ebp)
    movl    $LLSDA2, -64(%ebp)
    movl    $L6, -56(%ebp)
    movl    %esp, -52(%ebp)
    movl    %eax, (%esp)
    call    __Unwind_SjLj_Register
    movl    $1, -88(%ebp)
    call    __Z3barv
L1:
    movl    $-1, -88(%ebp)
    leal    -40(%ebp), %eax
    movl    %eax, (%esp)
    call    __ZN1CD1Ev
    leal    -92(%ebp), %eax
    movl    %eax, (%esp)
    call    __Unwind_SjLj_Unregister
    movl    -12(%ebp), %ebx
    movl    -8(%ebp), %esi
    movl    -4(%ebp), %edi
    movl    %ebp, %esp
    popl    %ebp
    ret
    .p2align 4,,7
L6:
L2:
L4:
    addl    $24, %ebp
    movl    $0, -88(%ebp)
    movl    -84(%ebp), %eax
    movl    %eax, -96(%ebp)
    leal    -40(%ebp), %eax
    movl    %eax, (%esp)
    call    __ZN1CD1Ev
    movl    $-1, -88(%ebp)
    movl    -96(%ebp), %eax
    movl    %eax, (%esp)
    call    __Unwind_SjLj_Resume
デストラクタはコンストラクタが呼び出されていなければ呼び出す必要はない(というか、呼び出してはいけない)ので、barを呼び出す前にコンストラクタが呼び出されていなければ、
class C {
  public:
    ~C();
};

void bar();

void foo()
{
    bar();  // 先にbarを呼んで、
    C c;    // それからCを作る。
}

余計なコードは生成されない。

__Z3foov:
L2:
    pushl   %ebp
    movl    %esp, %ebp
    subl    $40, %esp
    call    __Z3barv
    leal    -24(%ebp), %eax
    movl    %eax, (%esp)
    call    __ZN1CD1Ev
    leave
    ret

 あるいは、デストラクタを呼び終わった後にbarを呼べば、

class C {
  public:
    ~C();
};

void bar();

void foo()
{
    {
        C c;
    }   // ここでデストラクタは実行済み
    bar();
}

やっぱり短い。

__Z3foov:
L2:
    pushl   %ebp
    movl    %esp, %ebp
    leal    -24(%ebp), %eax
    subl    $40, %esp
    movl    %eax, (%esp)
    call    __ZN1CD1Ev
    call    __Z3barv
    leave
    ret

 動的に生成するオブジェクトは、関数を抜けるときにデストラクタが呼ばれるわけではないので、

class C {
  public:
    ~C();
};

void bar();

C *foo()
{
    C *pc = new C;
    bar();
    return pc;
}

 何をやったって短い。

__Z3foov:
    pushl   %ebp
    movl    %esp, %ebp
    pushl   %ebx
    subl    $4, %esp
    movl    $1, (%esp)
    call    __Znwj
    movl    %eax, %ebx
    call    __Z3barv
    popl    %edx
    movl    %ebx, %eax
    popl    %ebx
    popl    %ebp
    ret

 動的に生成したオブジェクトのデストラクタはdeleteの時に呼び出される。 例外でdeleteが飛ばされた場合は、どこか他でdeleteしなければならない約束になっているので、

class C {
  public:
    ~C();
};

void bar();

void foo()
{
    C *pc = new C;
    bar();
    delete pc;
}
__Z3foov:
    pushl   %ebp
    movl    %esp, %ebp
    pushl   %ebx
    subl    $4, %esp
    movl    $1, (%esp)
    call    __Znwj
    movl    %eax, %ebx
    call    __Z3barv
    testl   %ebx, %ebx
    je  L1
    movl    %ebx, (%esp)
    call    __ZN1CD1Ev
    movl    %ebx, (%esp)
    call    __ZdlPv
L1:
    popl    %eax
    popl    %ebx
    popl    %ebp
    ret

やっぱり余計なコードは生成されない。 ちなみに、「ポインタがNULLだったらdeleteは飛ばす」というコードも入っている。

 なんかややこしいようだけど、ルールは意外に単純。

 g++の場合、デストラクタのある自動変数の構築が終わってから破壊されるまでの間に、例外が起きる可能性があればスタック巻き戻しコードが生成される。

 メンバ変数のコンストラクタ・デストラクタは、親オブジェクトのコンストラクタ・デストラクタの中から呼び出される。 「デストラクタのあるメンバ」を持っているオブジェクトは、明示的にデストラクタを宣言していなくてもデストラクタがあることになる。 これは基底クラスがデストラクタを持っている場合と同じ。

 「関数の外で既に生成されているオブジェクト」のメンバを使う場合、元々の親オブジェクトが自動変数ならこの関数内で破壊されることはない。 引数でオブジェクトをもらう場合や、メンバ関数の場合によくあるパターンで、この場合、メンバはなんぼ使ってもスタック巻き戻しコードの有無に影響しない。 親オブジェクトが動的に生成された場合は、そもそもスタック巻き戻しには関係ない(deleteしない限りデストラクタは呼ばれないから)。 親オブジェクトがどこで生成されたか?が重要。

top

例外が起きる可能性のある関数

「例外が起きる可能性がある」というのは例外を投げる可能性のある関数を呼び出す、というのとほぼ同じ意味で、この「関数」にはコンストラクタやデストラクタも含まれる。だから、デストラクタのあるオブジェクトを作ってから、例外を投げる可能性のあるコンストラクタが実行されると、

class C {   // この人は例外を投げない。
  public:
    ~C() throw();
};

class D {
  public:
    D();    // 例外を投げるかもしれない。
};

void foo()
{
    C c;
    D d;    // コンストラクタが例外を投げるかも。
}

長くなる。

__Z3foov:
    pushl   %ebp
    movl    %esp, %ebp
    leal    -24(%ebp), %eax
    subl    $120, %esp
    movl    %eax, -60(%ebp)
    leal    -92(%ebp), %eax
    movl    %eax, (%esp)
    movl    %ebx, -12(%ebp)
    movl    %esi, -8(%ebp)
    movl    %edi, -4(%ebp)
    movl    $___gxx_personality_sj0, -68(%ebp)
    movl    $LLSDA2, -64(%ebp)
    movl    $L6, -56(%ebp)
    movl    %esp, -52(%ebp)
    call    __Unwind_SjLj_Register
    movl    $1, -88(%ebp)
    leal    -93(%ebp), %eax
    movl    %eax, (%esp)
    call    __ZN1DC1Ev
L1:
    leal    -40(%ebp), %eax
    movl    %eax, (%esp)
    call    __ZN1CD1Ev
    leal    -92(%ebp), %eax
    movl    %eax, (%esp)
    call    __Unwind_SjLj_Unregister
    movl    -12(%ebp), %ebx
    movl    -8(%ebp), %esi
    movl    -4(%ebp), %edi
    movl    %ebp, %esp
    popl    %ebp
    ret
    .p2align 4,,7
L6:
L2:
L4:
    addl    $24, %ebp
    movl    -84(%ebp), %eax
    movl    %eax, -100(%ebp)
    leal    -40(%ebp), %eax
    movl    %eax, (%esp)
    call    __ZN1CD1Ev
    movl    $-1, -88(%ebp)
    movl    -100(%ebp), %eax
    movl    %eax, (%esp)
    call    __Unwind_SjLj_Resume

 コンストラクタ中で例外が起きた場合は対応するデストラクタを呼び出さないお約束になっているので、一番最初に作成するオブジェクトのコンストラクタは例外を投げても、

class C {
  public:
    C();    // 例外を投げるかも
};

class D {
  public:
    ~D() throw();
};

void foo()
{
    C c;    // 例外を投げるかも
    D d;
}

このとおり。

__Z3foov:
L2:
    pushl   %ebp
    movl    %esp, %ebp
    leal    -25(%ebp), %eax
    subl    $56, %esp
    movl    %eax, (%esp)
    call    __ZN1CC1Ev
    leal    -24(%ebp), %eax
    movl    %eax, (%esp)
    call    __ZN1DD1Ev
    leave
    ret

 デストラクタについても同様のことが言えて、一番最後に実行されるデストラクタが例外を投げても、スタック巻き戻しコードは生成されない。 デストラクタはコンストラクタの逆の順序で実行されるので、要するに一番最初に構築されるオブジェクトはコンストラクタ・デストラクタが例外を投げても

class C {
  public:
    C();    // 例外を投げるかも
    ~C();   // 例外を投げるかも
};

class D {
  public:
    ~D() throw();
};

void foo()
{
    C c;    // c.C() が実行される・・・例外を投げるかも
    D d;
    // d.~D() が実行される
    // c.~C() が実行される・・・例外を投げるかも
}

こんな感じで済む。

__Z3foov:
L2:
L4:
    pushl   %ebp
    movl    %esp, %ebp
    pushl   %ebx
    subl    $52, %esp
    leal    -24(%ebp), %ebx
    movl    %ebx, (%esp)
    call    __ZN1CC1Ev
    leal    -40(%ebp), %eax
    movl    %eax, (%esp)
    call    __ZN1DD1Ev
    movl    %ebx, (%esp)
    call    __ZN1CD1Ev
    addl    $52, %esp
    popl    %ebx
    popl    %ebp
    ret
top

try ... catch

try ... catch で例外を捕捉すると、実行すべきデストラクタがなくても、

void bar();

void foo()
{
    try {
        bar();
    }
    catch (...) {
    }
}

長い。

__Z3foov:
    pushl   %ebp
    movl    %esp, %ebp
    leal    -12(%ebp), %eax
    subl    $88, %esp
    movl    %eax, -32(%ebp)
    leal    -64(%ebp), %eax
    movl    %ebx, -12(%ebp)
    movl    %esi, -8(%ebp)
    movl    %edi, -4(%ebp)
    movl    $___gxx_personality_sj0, -40(%ebp)
    movl    $LLSDA2, -36(%ebp)
    movl    $L7, -28(%ebp)
    movl    %esp, -24(%ebp)
    movl    %eax, (%esp)
    call    __Unwind_SjLj_Register
    movl    $1, -60(%ebp)
    call    __Z3barv
L1:
    leal    -64(%ebp), %eax
    movl    %eax, (%esp)
    call    __Unwind_SjLj_Unregister
    movl    -12(%ebp), %ebx
    movl    -8(%ebp), %esi
    movl    -4(%ebp), %edi
    movl    %ebp, %esp
    popl    %ebp
    ret
    .p2align 4,,7
L7:
L3:
L4:
    addl    $12, %ebp
    movl    -56(%ebp), %eax
    movl    %eax, (%esp)
    call    ___cxa_begin_catch
    movl    $-1, -60(%ebp)
    call    ___cxa_end_catch
    leal    -64(%ebp), %eax
    movl    %eax, (%esp)
    call    __Unwind_SjLj_Unregister
    movl    -12(%ebp), %ebx
    movl    -8(%ebp), %esi
    movl    -4(%ebp), %edi
    movl    %ebp, %esp
    popl    %ebp
    ret

 でも、tryの中で例外が出なければ、

void bar() throw();

void foo()
{
    try {
        bar();  // 実は例外は発生しない
    }
    catch (...) {
    }
}

短くなる。

__Z3foov:
    pushl   %ebp
    movl    %esp, %ebp
    popl    %ebp
    jmp __Z3barv

まあ、最初から例外が出ないと分かっているなら、try ... catchはまったく意味ないけど。ちなみに、この例では最後のcall/retがjmpに置き換えられてる。

top

例外と標準クラス

 たいていの標準クラスは例外を投げる、かもしれない。

#include <vector>

class C {
  public:
    ~C();
};

void foo(std::vector<int> &rv)
{
    C c;
    rv.push_back(1);    // メモリが足りないと例外を投げる
}
__Z3fooRSt6vectorIiSaIiEE:
    pushl   %ebp
    movl    %esp, %ebp
    subl    $120, %esp
    movl    %esp, -52(%ebp)
    movl    8(%ebp), %eax
    movl    %ebx, -12(%ebp)
    movl    %esi, -8(%ebp)
    movl    %eax, -100(%ebp)
    leal    -24(%ebp), %eax
    movl    %eax, -60(%ebp)
    leal    -92(%ebp), %eax
    movl    %eax, (%esp)
    movl    %edi, -4(%ebp)
    movl    $___gxx_personality_sj0, -68(%ebp)
    movl    $LLSDA445, -64(%ebp)
    movl    $L117, -56(%ebp)
    call    __Unwind_SjLj_Register
    movl    $1, -96(%ebp)
    movl    -100(%ebp), %eax
    movl    4(%eax), %edx
    cmpl    8(%eax), %edx
    je  L100
L102:
    testl   %edx, %edx
    jne L118
L106:
L108:
    movl    -100(%ebp), %eax
    addl    $4, 4(%eax)
L114:
L99:
    movl    $-1, -88(%ebp)
    leal    -40(%ebp), %eax
    movl    %eax, (%esp)
    call    __ZN1CD1Ev
    leal    -92(%ebp), %eax
    movl    %eax, (%esp)
    call    __Unwind_SjLj_Unregister
    movl    -12(%ebp), %ebx
    movl    -8(%ebp), %esi
    movl    -4(%ebp), %edi
    movl    %ebp, %esp
    popl    %ebp
    ret

が、std::auto_ptrはほとんどのメンバ関数が例外を投げない

#include <memory>

class C {
  public:
    ~C();
};

void bar(int *) throw();    // 例外は投げない

void foo(std::auto_ptr<int> &rv)
{
    C c;
    bar(rv.get());  // 例外は投げない
}
__Z3fooRSt8auto_ptrIiE:
L3:
L5:
L7:
    pushl   %ebp
    movl    %esp, %ebp
    subl    $40, %esp
    movl    8(%ebp), %eax
    movl    (%eax), %eax
    movl    %eax, (%esp)
    call    __Z3barPi
    leal    -24(%ebp), %eax
    movl    %eax, (%esp)
    call    __ZN1CD1Ev
    leave
    ret

でもg++の場合、デストラクタだけは例外を投げるかもしれない。 ヘッダファイルに書いてあるコメントを見ると、故意にそうしてあるようだ。 具体的には、auto_ptrのテンプレートパラメータに指定したクラスのデストラクタが例外を投げる可能性があると、

#include <memory>

class D {
  public:
    ~D();   // 例外を投げるかも。
};

class C {
  public:
    ~C() throw();
};

void foo()
{
    C c;
    std::auto_ptr<D> ap;
    // ap.~auto_ptr() は例外を投げるかもしれない。
}

こんなん出てきます。

__Z3foov:
L2:
L5:
L16:
L12:
L14:
L1:
    pushl   %ebp
    movl    %esp, %ebp
    leal    -24(%ebp), %eax
    subl    $136, %esp
    movl    %eax, -76(%ebp)
    leal    -108(%ebp), %eax
    movl    %ebx, -12(%ebp)
    movl    %esi, -8(%ebp)
    movl    %edi, -4(%ebp)
    movl    %esp, -68(%ebp)
    movl    %eax, (%esp)
    movl    $___gxx_personality_sj0, -84(%ebp)
    movl    $LLSDA402, -80(%ebp)
    movl    $L16, -72(%ebp)
    call    __Unwind_SjLj_Register
    movl    $0, -56(%ebp)
    leal    -40(%ebp), %eax
    movl    %eax, (%esp)
    call    __ZN1CD1Ev
    leal    -108(%ebp), %eax
    movl    %eax, (%esp)
    call    __Unwind_SjLj_Unregister
    movl    -12(%ebp), %ebx
    movl    -8(%ebp), %esi
    movl    -4(%ebp), %edi
    movl    %ebp, %esp
    popl    %ebp
    ret

デストラクタが例外を投げないと分かっていれば、インライン展開されて、

#include <memory>

class D {
  public:
    D();    // コンストラクタは関係ない
    ~D() throw();
};

class C {
  public:
    ~C();
};

void foo()
{
    C c;
    std::auto_ptr<D> ap;
}

すっきりしてしまう。

__Z3foov:
L2:
L5:
L12:
L14:
    pushl   %ebp
    movl    %esp, %ebp
    leal    -24(%ebp), %eax
    subl    $56, %esp
    movl    %eax, (%esp)
    movl    $0, -40(%ebp)
    call    __ZN1CD1Ev
    leave
    ret
top

例外を投げないはずの関数が例外を投げる

 例外を投げない、と指定した関数から例外が投げられると、unexpected関数が呼ばれ、この関数はデフォルトではterminateを呼び出し、さらにabortなどが呼ばれてそこでプログラムの実行が終了する。 例えば、このプログラムは、

#include <iostream>

void bar()
{
    std::cout << "bar{ ";
    throw 1;
    std::cout << "}bar ";
}

void foo()
{
    std::cout << "foo{ ";
    bar();
    std::cout << "}foo ";
}

int main()
{
    std::cout << "main{ ";
    try {
        std::cout << "try{ ";
        foo();
        std::cout << "}try ";
    }
    catch (...) {
        std::cout << "CATCH! ";
    }
    std::cout << "}main\n";
    return 0;
}
main{ try{ foo{ bar{ CATCH! }main
こんな感じで普通に終了するが、fooは例外を投げない! と指定すると、
#include <iostream>

void bar()
{
    std::cout << "bar{ ";
    throw 1;
    std::cout << "}bar ";
}

void foo() throw()
{
    std::cout << "foo{ ";
    bar();
    std::cout << "}foo ";
}

int main()
{
    std::cout << "main{ ";
    try {
        std::cout << "try{ ";
        foo();
        std::cout << "}try ";
    }
    catch (...) {
        std::cout << "CATCH! ";
    }
    std::cout << "}main\n";
    return 0;
}
main{ try{ foo{ bar{      34 [sig] a 3488 _cygtls::handle_exceptions: Error while dumping state (probably corrupted stack)
Segmentation fault (core dumped)

コアをお吐きになります。 ちなみに、関数fooのアセンブラソースは、throw()がなければ、

__Z3foov:
    pushl   %ebp
    movl    $LC2, %eax
    movl    %esp, %ebp
    subl    $8, %esp
    movl    %eax, 4(%esp)
    movl    $__ZSt4cout, (%esp)
    call    __ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc
    call    __Z3barv
    movl    $__ZSt4cout, (%esp)
    movl    $LC3, %eax
    movl    %eax, 4(%esp)
    call    __ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc
    leave
    ret

と短く済むが、throw()を付けると、

__Z3foov:
    pushl   %ebp
    movl    %esp, %ebp
    leal    -12(%ebp), %eax
    subl    $88, %esp
    movl    %eax, -32(%ebp)
    leal    -64(%ebp), %eax
    movl    %eax, (%esp)
    movl    %ebx, -12(%ebp)
    movl    %esi, -8(%ebp)
    movl    %edi, -4(%ebp)
    movl    $___gxx_personality_sj0, -40(%ebp)
    movl    $LLSDA1381, -36(%ebp)
    movl    $L8, -28(%ebp)
    movl    %esp, -24(%ebp)
    call    __Unwind_SjLj_Register
    movl    $__ZSt4cout, (%esp)
    movl    $LC2, %eax
    movl    %eax, 4(%esp)
    movl    $1, -60(%ebp)
    call    __ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc
    call    __Z3barv
    movl    $__ZSt4cout, (%esp)
    movl    $LC3, %eax
    movl    %eax, 4(%esp)
    call    __ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc
L4:
    leal    -64(%ebp), %eax
    movl    %eax, (%esp)
    call    __Unwind_SjLj_Unregister
    movl    -12(%ebp), %ebx
    movl    -8(%ebp), %esi
    movl    -4(%ebp), %edi
    movl    %ebp, %esp
    popl    %ebp
    ret
    .p2align 4,,7
L8:
    addl    $12, %ebp
    cmpl    $-1, -52(%ebp)
    movl    -56(%ebp), %eax
    je  L5
    movl    %eax, (%esp)
    movl    $-1, -60(%ebp)
    call    __Unwind_SjLj_Resume
    .p2align 4,,7
L5:
    movl    %eax, (%esp)
    movl    $-1, -60(%ebp)
    call    ___cxa_call_unexpected

と、スタック巻き戻しコードが入る。どうもfooで例外をフックして、cxa_call_unexpectedを呼び出しているようだ。 unexpectedはset_unexpectedで挙動を変えることができるが、そのためのラッパがcxa_call_unexpectedなのだろう。

top

-fno-exceptions

 いちいちthrow()を書くのがめんどうだ、という向きには、-fno-exceptions というオプションがある。 このオプションはg++のものだが、他のコンパイラにも同様のオプションがあるようだ。 MSVCの場合、少なくともMSVS.2003までは、何も指定しない=スタック巻き戻しを行わない、だった。

 例えば、冒頭に挙げたコードを、g++ -O2 -fno-exceptions -S でコンパイルすると、throw()を書いたのと同じコードになる。

__Z3foov:
    pushl   %ebp
    movl    %esp, %ebp
    subl    $40, %esp
    call    __Z3barv
    leal    -24(%ebp), %eax
    movl    %eax, (%esp)
    call    __ZN1CD1Ev
    leave
    ret

 -fno-exceptionsを付けた部分ではthrow・catchが使えない。

void foo()
{
    throw 1;
}
$ g++ -O2 -fno-exceptions -c xcp.cpp
xcp2.cpp: In function `void foo()':
xcp2.cpp:3: error: exception handling disabled, use -fexceptions to enable

-fno-exceptionsを付けたコードと、付けないコードは一応共存できるようだ。

// xcp2.cpp: -fno-exceptions でコンパイル
#include <iostream>

void bar(); // 例外を投げるかもしれない

void foo() throw()  // 例外は投げない?
{
    std::cout << "foo{ ";
    bar();  // いや、やっぱり投げるのか?!
    std::cout << "}foo ";
}
// xcp.cpp: 普通にコンパイル
#include <iostream>

void bar()
{
    std::cout << "bar{ ";
    throw(1);   // 投げちゃった。
    std::cout << "}bar ";
}

void foo() throw();

int main()
{
    std::cout << "main{ ";
    try {
        std::cout << "try{ ";
        foo();
        std::cout << "}try ";
    }
    catch (...) {
        std::cout << "CATCH! ";
    }
    std::cout << "}main\n";
}

例外を出さないはずの先ほどの例と違って、unexpectedは呼ばれず、普通に終了する。

$ g++ -c -O2 -fno-exceptions xcp2.cpp
$ g++ -O2 -o xcp xcp.cpp xcp2.o
$ ./xcp
main{ try{ foo{ bar{ CATCH! }main

 ただし、xcp2.cppでは一切の例外コードが出力されない。 スタック巻き戻しコードも生成されないので、例外が起きた場合にxcp2.cpp内のデストラクタが呼ばれなくなる

// xcp2.cpp: -fno-exceptions でコンパイル
#include <iostream>

void bar();

class D {
  public:
    D() throw() { std::cout << "D::D() "; }
    ~D() throw() { std::cout << "D::~D() "; }
};

void foo()
{
    std::cout << "foo{ ";
    D d;
    bar();
    std::cout << "}foo ";
}
// xcp.cpp: 普通にコンパイル
#include <iostream>

class C {
  public:
    C(const char *s) throw() : ms(s) { std::cout << ms << "::C() "; }
    ~C() throw() { std::cout << ms << "::~C() "; }
    const char *ms;
};

void bar()
{
    std::cout << "bar{ ";
    C c1("c1");
    throw(1);
    std::cout << "}bar ";
}

void foo();

int main()
{
    std::cout << "main{ ";
    try {
        std::cout << "try{ ";
        C c2("c2");
        foo();
        std::cout << "}try ";
    }
    catch (...) {
        std::cout << "CATCH! ";
    }
    std::cout << "}main\n";
}

こんなコードを以下のようにコンパイルして実行すると、

$ g++ -c -O2 -fno-exceptions xcp2.cpp
$ g++ -O2 -o xcp xcp.cpp xcp2.o
$ ./xcp
main{ try{ c2::C() foo{ D::D() bar{ c1::C() c1::~C() c2::~C() CATCH! }main

Dのコンストラクタに対応するデストラクタが実行されていないことが分かる。

top

例外指定と仮想関数

 仮想関数に例外指定をした場合、派生クラスでオーバーライドする関数は、それよりもより厳しい(少ない)例外指定しかできない。 だから、仮想関数を throw() にしてしまうと、派生クラスでオーバーライドする仮想関数も throw() にしなければならない。

class C {
  public:
    virtual void vf() throw();
};

class D : public C {
  public:
    virtual void vf();
};
xcp2.cpp:8: error: looser throw specifier for `virtual void D::vf()'
xcp2.cpp:3: error:   overriding `virtual void C::vf() throw ()'

これは警告ではなくエラーである。 なので、仮想関数は例外指定はしない方がよさそうだ。

top

まとめ

 実行速度に効いてくるループの一番内側などは、例外コードに細心の注意をはらう必要がある。 ループの一番内側から関数を呼び出す場合、その関数でデストラクタのあるオブジェクトを作ってから破壊するまでの間に、例外が発生する可能性のある関数を呼んでしまうと、一番内側のループが一回まわるごとにスタック巻き戻しコードが実行されてしまう。 例えば、以下のコードはものすごく損である。

class C {
  public:
    C();
    ~C();
};

void bar();

void foo()
{
    C c;
    bar();
}

void hoge()
{
    for (int i = 0; i < 10000; i++)
        foo();
}
__Z3foov:
    pushl   %ebp
    movl    %esp, %ebp
    leal    -24(%ebp), %eax
    subl    $120, %esp
    movl    %eax, -60(%ebp)
    leal    -92(%ebp), %eax
    movl    %eax, (%esp)
    movl    %ebx, -12(%ebp)
    movl    %esi, -8(%ebp)
    movl    %edi, -4(%ebp)
    movl    $___gxx_personality_sj0, -68(%ebp)
    movl    $LLSDA2, -64(%ebp)
    movl    $L6, -56(%ebp)
    movl    %esp, -52(%ebp)
    call    __Unwind_SjLj_Register
    movl    $-1, -88(%ebp)
    leal    -40(%ebp), %eax
    movl    %eax, (%esp)
    call    __ZN1CC1Ev
    movl    $1, -88(%ebp)
    call    __Z3barv
L1:
    movl    $-1, -88(%ebp)
    leal    -40(%ebp), %eax
    movl    %eax, (%esp)
    call    __ZN1CD1Ev
    leal    -92(%ebp), %eax
    movl    %eax, (%esp)
    call    __Unwind_SjLj_Unregister
    movl    -12(%ebp), %ebx
    movl    -8(%ebp), %esi
    movl    -4(%ebp), %edi
    movl    %ebp, %esp
    popl    %ebp
    ret
----- 8< ----- 8< ----- 中略 ----- 8< ----- 8< -----
__Z4hogev:
    pushl   %ebp
    movl    %esp, %ebp
    pushl   %ebx
    subl    $4, %esp
    movl    $9999, %ebx
    .p2align 4,,15
L12:
    call    __Z3foov
    decl    %ebx
    jns     L12
    popl    %eax
    popl    %ebx
    popl    %ebp
    ret

「中略」の上が関数fooだが、この中には分岐命令は入っていないので、fooが呼ばれるたびにこのコードが全部実行される。 つまり、こんなデカいものが10,000回実行される。 これを、barが例外を投げないようにすると、

class C {
  public:
    C();
    ~C();
};

void bar() throw();

void foo()
{
    C c;
    bar();
}

void hoge()
{
    for (int i = 0; i < 10000; i++)
        foo();
}
__Z3foov:
L2:
    pushl   %ebp
    movl    %esp, %ebp
    pushl   %ebx
    subl    $36, %esp
    leal    -24(%ebp), %ebx
    movl    %ebx, (%esp)
    call    __ZN1CC1Ev
    call    __Z3barv
    movl    %ebx, (%esp)
    call    __ZN1CD1Ev
    addl    $36, %esp
    popl    %ebx
    popl    %ebp
    ret
----- 8< ----- 8< ----- 中略 ----- 8< ----- 8< -----
__Z4hogev:
    pushl   %ebp
    movl    %esp, %ebp
    pushl   %ebx
    subl    $4, %esp
    movl    $9999, %ebx
    .p2align 4,,15
L8:
    call    __Z3foov
    decl    %ebx
    jns L8
    popl    %eax
    popl    %ebx
    popl    %ebp
    ret

ほぼ必要なコードだけになる。 あるいは、クラスCのデストラクタをなくしてもよい。

class C {
  public:
    C();
    // デストラクタを削除
};

void bar(); // 例外を投げるかもしれない

void foo()
{
    C c;
    bar();
}

void hoge()
{
    for (int i = 0; i < 10000; i++)
        foo();
}
__Z3foov:
    pushl   %ebp
    movl    %esp, %ebp
    leal    -1(%ebp), %eax
    subl    $8, %esp
    movl    %eax, (%esp)
    call    __ZN1CC1Ev
    call    __Z3barv
    leave
    ret
----- 8< ----- 8< ----- 中略 ----- 8< ----- 8< -----
__Z4hogev:
    pushl   %ebp
    movl    %esp, %ebp
    pushl   %ebx
    subl    $4, %esp
    movl    $9999, %ebx
    .p2align 4,,15
L6:
    call    __Z3foov
    decl    %ebx
    jns L6
    popl    %eax
    popl    %ebx
    popl    %ebp
    ret

cの生成とbarの呼び出しを逆にする、という手もある。

class C {
  public:
    C();
    ~C();
};

void bar();

void foo()
{
    bar();
    C c;
}

void hoge()
{
    for (int i = 0; i < 10000; i++)
        foo();
}
__Z3foov:
L2:
    pushl   %ebp
    movl    %esp, %ebp
    pushl   %ebx
    subl    $36, %esp
    leal    -24(%ebp), %ebx
    call    __Z3barv
    movl    %ebx, (%esp)
    call    __ZN1CC1Ev
    movl    %ebx, (%esp)
    call    __ZN1CD1Ev
    addl    $36, %esp
    popl    %ebx
    popl    %ebp
    ret
----- 8< ----- 8< ----- 中略 ----- 8< ----- 8< -----
__Z4hogev:
    pushl   %ebp
    movl    %esp, %ebp
    pushl   %ebx
    subl    $4, %esp
    movl    $9999, %ebx
    .p2align 4,,15
L8:
    call    __Z3foov
    decl    %ebx
    jns L8
    popl    %eax
    popl    %ebx
    popl    %ebp
    ret

 どれもできない場合、関数の規模が小さければインラインにしてしまうという手もある。

class C {
  public:
    C();
    ~C();
};

void bar();

inline void foo()
{
    C c;
    bar();
}

void hoge()
{
    for (int i = 0; i < 10000; i++)
        foo();
}
__Z4hogev:
    pushl   %ebp
    movl    %esp, %ebp
    leal    -24(%ebp), %eax
    pushl   %edi
    pushl   %esi
    pushl   %ebx
    subl    $108, %esp
    movl    %eax, -60(%ebp)
    leal    -92(%ebp), %eax
    movl    $___gxx_personality_sj0, -68(%ebp)
    movl    $LLSDA3, -64(%ebp)
    movl    $L11, -56(%ebp)
    movl    %esp, -52(%ebp)
    movl    %eax, (%esp)
    call    __Unwind_SjLj_Register
    movl    $0, -96(%ebp)
L10:
    movl    $-1, -88(%ebp)
    leal    -40(%ebp), %eax
    movl    %eax, (%esp)
    call    __ZN1CC1Ev
    movl    $1, -88(%ebp)
    call    __Z3barv
    movl    $-1, -88(%ebp)
    leal    -40(%ebp), %eax
    movl    %eax, (%esp)
    call    __ZN1CD1Ev
    incl    -96(%ebp)
    cmpl    $9999, -96(%ebp)
    jle L10
L1:
    leal    -92(%ebp), %eax
    movl    %eax, (%esp)
    call    __Unwind_SjLj_Unregister
    addl    $108, %esp
    popl    %ebx
    popl    %esi
    popl    %edi
    popl    %ebp
    ret

スタック巻き戻しのためのコードは生成されるが、ループはL10からL1の直前までなので、hogeの呼び出し1回につき、スタック巻き戻しコードは1回しか実行されない。 最初の例に比べてずっと速くなる。

top

その他

 浮動小数の数値演算関数については、問題が発生しても例外は投げず、nanとかinfとかが返ってくる、みたいだ。 整数の場合は問題があると落ちる場合がある。 一番分かりやすいのは「ゼロによる除算」だろう。

#include <stdlib.h>
#include <iostream>

int main(int argc, char *argv[])
{
    std::cout << 1000 / atof(argv[1]) << "\n";
    return 0;
}

このプログラムをコマンドライン引数に0を与えて実行すると、

inf

と表示されるが、atof を atoi にすると、「ゼロによる除算」の例外で落ちる。 これを引っ掛けられるかどうかは不明だが、例外で引っ掛けるより割り算する前に除数がゼロでないことを調べる方がはるかに楽なので、割り算を見たら分母がゼロになる可能性がないかをまず疑うクセを付けておくとよい。

 まあしかし、浮動小数の場合でも、ライブラリによっては例外を投げるかもしれないので、除算以外にも、sqrt・log・acosなど、定義域が制限されている関数を使う場合には、関数を呼び出す前に引数を確認するクセを付けたほうがよい。 top

Copyright (C) 2008-2011 You SUZUKI

$Id: exception.htm,v 1.8 2011/04/09 15:24:39 you Exp $