例外機構 -- throw されたオブジェクトの行方

例外処理に際して、throw された例外オブジェクトがどのように処理されているかを知るために、 次のようなプログラムを動かしてみた。

#include <iostream>
using std::cout;
using std::endl;

// 例外オブジェクトのクラス
class Ex {
public:
    // コンストラクタ
    Ex() {
        cout << "Ex ctor" << endl;
    }
    // デストラクタ
    ~Ex() {
        cout << "Ex dtor" << endl;
    }
    // コピーコンストラクタ
    Ex(const Ex& e) {
        cout << "Ex copy ctor" << endl;
    }
    // 代入
    const Ex& operator=(const Ex& e) {
        cout << "Ex assignment" << endl;
        return *this;
    }
};

void foo()
{
    cout << "Enter foo" << endl;
    // 例外の throw
    throw Ex();
    cout << "Leave foo" << endl;
}

int main()
{
    try {
        foo();
    }
    catch (Ex e) {
        cout << "catch" << endl;
    }
    cout << "return" << endl;
    return 0;
}  

結果は次のとおり。

Enter foo     ; 関数 foo() に入った
Ex ctor       ; Ex(); で新しくオブジェクトが生成された (a)
Ex copy ctor  ; throw で、(a) のオブジェクトのコピーが作成された (b)
Ex dtor       ; 関数 foo() を出るときに (a) のオブジェクトが破棄された
Ex copy ctor  ; catch (Ex e) で、(b) のオブジェクトのコピーが作成された (c)
catch         ; catch ブロック内に入った
Ex dtor       ; catch ブロックを出るときに (c) のオブジェクトが破棄された
Ex dtor       ; 例外処理が完了して、(b) のオブジェクトが破棄された
return   

図示するとこんな感じかな。 (図が崩れるようならフォントサイズを変えてみてくだされ)

  func()
 ブロック
┌───┐
│      │
│      │        例外機構
│ (a)  │      ┌──────────┐
│ Ex() │throw │ (b)        catch   │
│  ┠─┼───┼─┨       ブロック │
│  ┃  │コピー│  ┃      ┌───┐│
│~Ex() │      │  ┃catch │ (c)  ││
└───┘      │  ┠───┼─┨  ││
                │  ┃コピー│  ┃  ││
                │  ┃      │  ┃  ││
                │  ┃      │~Ex() ││
                │  ┃      └───┘│
                │~Ex()               │
                └──────────┘  

catch ブロック内で例外オブジェクトの内容を変更する必要がないなら、

  catch (const Ex& e)

のようにリファレンスで受け取ったほうがよいだろう。 そうすると (c) のオブジェクトは生成されずに、(b) のオブジェクトが catch ブロック内からも参照される。

一方、throw のところは常に値渡しになる。 つまり必ずコピーコンストラクタが呼ばれるということ。 だから、

  static Ex ex;
  Ex& rex = ex;
  throw rex;   

のように参照を throw しようとしても無駄。やっぱりコピーコンストラクタが呼ばれてしまうのだ。 throw のところでどうしてもオブジェクトのコピーを避けたいなら、 次のようにポインタを throw するという方法がある。

  void foo()
  {
    …
    static Ex ex;
    throw &ex;
  }

  {
    try {
      foo();
    }
    catch (const Ex *pe) {
      …
    }
  } 

当然、catch のほうもポインタを受け取るように修正しなければならない。

しかし、ポインタを throw する方法は、はっきりいっておすすめしない。 理由は、リソースリークを起こす原因になりやすいからだ。 上のように静的に確保したオブジジェクトのポインタを throw するのならよいが、

  throw new Ex;

のように動的に確保したオブジェクトのポインタを throw した場合は、 catch 側で明示的に delete してやらないと、 オブジェクトがゴミとして取り残されてしまう。また始末が悪いことに、catch 側では、 渡ってきたポインタの指しているオブジェクトが、静的に確保されたものなのか、 それとも new で動的に確保されたものなのか区別できない。 動的なオブジェクトを delete し忘れるとリソースリークが起きるし、 静的なオブジェクトを delete してしまうと、たぶんプログラムはクラッシュする。 いずれにしても良いことは何一つない。

そもそも例外処理にある程度の時間がかかるのはやむをえないことなのである。 ここで必要以上に処理時間を気にしてもしかたがない。 パフォーマンスを上げるためにチューニングすべき場所は、他にもっといっぱいあるはずだ。 ……幾重にもネストしたループから脱出するのに、まさか try-throw-catch を使ってたりしないよね?


初出: 2002年6月29日

C++ ラビリンスの目次へ


ホームページへ / Last modified: 2002-07-01 01:11:35 JST
Created by OKA Toshiyuki < oka-t@fides.dti.ne.jp >