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
ある関数で発生するかもしれない例外は、例外指定として書いておくことができる。 これを使って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
スタックの巻き戻し処理は、デストラクタを呼び出すためのものだから、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しない限りデストラクタは呼ばれないから)。 親オブジェクトがどこで生成されたか?が重要。
「例外が起きる可能性がある」というのは例外を投げる可能性のある関数を呼び出す、というのとほぼ同じ意味で、この「関数」にはコンストラクタやデストラクタも含まれる。だから、デストラクタのあるオブジェクトを作ってから、例外を投げる可能性のあるコンストラクタが実行されると、
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
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に置き換えられてる。
たいていの標準クラスは例外を投げる、かもしれない。
#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
例外を投げない、と指定した関数から例外が投げられると、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なのだろう。
いちいち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のコンストラクタに対応するデストラクタが実行されていないことが分かる。
仮想関数に例外指定をした場合、派生クラスでオーバーライドする関数は、それよりもより厳しい(少ない)例外指定しかできない。 だから、仮想関数を 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 ()'
これは警告ではなくエラーである。 なので、仮想関数は例外指定はしない方がよさそうだ。
実行速度に効いてくるループの一番内側などは、例外コードに細心の注意をはらう必要がある。 ループの一番内側から関数を呼び出す場合、その関数でデストラクタのあるオブジェクトを作ってから破壊するまでの間に、例外が発生する可能性のある関数を呼んでしまうと、一番内側のループが一回まわるごとにスタック巻き戻しコードが実行されてしまう。 例えば、以下のコードはものすごく損である。
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回しか実行されない。 最初の例に比べてずっと速くなる。
浮動小数の数値演算関数については、問題が発生しても例外は投げず、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 $