第4回 例外処理とその扱い

例外処理のおさらい

今回は、例外処理についてかなり突っ込んだお話をしたいと思います。本題に入る前に、まずは例外処理がどんなのものであるか今一度確認しておきます。

#include <stdio.h>

class X {};

void foo()
{
  X x;
  throw x;
}

int main()
{
  try
  {
    foo();
    puts("A");
  }
  catch (X& e)
  {
    puts("B");
  }
  return 0;
}

といったサンプルプログラムを実行すると、"A" ではなく、"B" が出力されます。これは、foo の中で x を例外として送出しているからで、関数呼び出しツリーのどんなに深いところからでも、例外が発生すれば一気に元のところまで制御が移ります。この点においては、setjmp/longjmp と非常に良く似ています。

例外が setjmp/longjmp と最も違うのは、非局所分岐によってスタックを巻き戻す際、途中にあった自動記憶域期間のオブジェクトのデストラクタを順に呼び出すことができる点です。このようにすることで、途中にあったオブジェクトが解体されることなくさまようことを防げます。

例外処理機構を持つ言語は他にもたくさんありますが、それらの多くは、try/catch の他に finally 節があり、そこでリソースの解放等を行うようにしています。しかし、そのような方法では、解放すべき対象が複数あると try/catch/finally を入れ子にするしかありません。C++ では、例外処理とデストラクタという、非常に相性の良い読み合わせによって、非常にすっきりした記述ができるようになっています。

例外処理をオフにする

C++が例外処理とデストラクタを持っていることは大きな強みですが、それは同時に大きな代償を払うことにもつながっています。開発者がC++の例外について十分理解していない場合、処理系に例外処理をオフにする機能があるなら、オフにした方が無難かもしれません。

コンパイラは、(例外の送出に伴う)スタックの巻き戻し時に、どんなデストラクタを呼び出すべきかの情報をブロックごとに生成しなければなりません。また、通常はデストラクタを呼び出すためのコードも生成する必要があります。これは、下手をすれば関数本体よりも大きなサイズになることがあります。

このように、例外処理はプログラムサイズを大幅に肥大化させます。PC 等のプログラムであればプログラムサイズはそれほどシビアではないかもしれませんが、組み込み環境では致命傷にさえなりかねません。これが例外処理をオフにすべき第一の理由です。

例外処理をオフにすべき第二の理由は、例外安全に関わる問題です。例外安全はかなり難度の高い話題でもあるので、詳細は別の機会に譲るとして、ごく大雑把にいえば、例外が発生したことでリソースリークやその他のリカバリ不能な事態に陥らない設計が必要というです。
例えば次のコードをご覧ください。

void foo()
{
  X* p = new X;
  hoge(p);
  delete p;
}

非常に単純なコードですが、このコードは例外に対して安全ではありません。なぜなら、もし hoge が例外を送出すれば、次の行の delete p; は実行される機会を失い、p が指すインスタンスはリークするからです。

C++ では、スカラ型の操作や明示的に例外が送出されないことを指定された関数以外は、すべて例外が送出される可能性があります。仮に関数の実装を紐解いて、throw がないことを確認しても、将来にわたって保証されるわけではありません。演算子までもがそうなのです。

C であっても、返却値等でエラーコードを返す場合、返却値型が void でない限り、すべての関数の返却値をチェックしなければなりませんが、C++ の例外も結局は同じで、チェックコードは書かなくても構いませんが、配慮は必要だということです。

そして、返却値で返されたエラーコードを無視してもコンパイラは何もいわないのと同様、例外安全でないコードを書いてもコンパイラは何も教えてくれません。例外安全への対処は、ある程度は体で覚えなければならないことなのです。

このように例外安全への配慮というのは、高い技術力が要求されます。これについてはビジネスアプリケーション等でも同じですが、組み込みではより高い信頼性が要求されるため、十分な注意が必要となります。

例外処理をオフにすべき理由はもうひとつあります。それは、組み込み用の汎用のコンパイラは、例外処理に関してマルチタスク環境に対応していないということです。OS なしの場合や、Linux や VxWorks のようなハイエンドな環境であれば問題ありませんが μITRON 等を使う場合は大問題です。

これは TOPPERS/JSP カーネルのように、例外処理を含めて C++ に対応した OS を使うか、コンパイラを改造するか(ABI 関数の再実装で済む場合もある)、C++ を使うタスクを 1 つに制限するしか対策がありません。

こんな風に書くと、例外は怖いので決して使うべきではないと短絡的な発想になってしまうかもしれません。しかし、思い出してみてください。多くの方は、C を覚えたての頃にポインタの危険性に冷や冷やしながらも、いったん使いこなせればこんな強力な武器はないことに気づいたはずです。

例外処理も、そう意味ではポインタと同じようなもので、しっかりと理解し、使いこなせるようになるまで体で覚えれば、これほど強力な武器はありません。

_

> このように例外安全への配慮というのは、高い技術力が要求されます。
> これについてはビジネスアプリケーション等でも同じですが、組み込みでは
> より高い信頼性が要求されるため、十分な注意が必要となります。

これはエラーを戻り値で通知~捕捉しても問題になるので、言語の例外機能を
オフにしてどうにかできる問題ではないと思います。

例外を使わない場合はエラーを無視しても何も起こらないので、必要な技術力を
身に付ける機会が遅れるという問題も考えられ、この点ではむしろ例外のほうが
優れているようにも思います。

例外安全性を問題として例外機能をオフにするというのは筋違いな対応なのでは
ないでしょうか?

コメントありがとうございます。

> 例外を使わない場合はエラーを無視しても何も起こらないので、必要な技術力を
> 身に付ける機会が遅れるという問題も考えられ、この点ではむしろ例外のほうが
> 優れているようにも思います。

基本的にはご指摘のとおりです。
ただ、(やや言葉足らずではありますが)Cでは適切にプログラミングできることを前提に書いているところがありますので、このような表現になっています。
つまり、返却値としてエラーコードを返すなど、Cと同じ方法であれば間違わないプログラマであっても、例外によって生じる実行パスを見極めるのは容易ではないなど、不慣れな故に問題が起きやすいことを指摘しています。

このエントリーを含むはてなブックマーク