newの罠

先日話題になった、C++のコンストラクタにおける例外の話のまとめ。元ネタはこれ。

で、例外話と言えばExceptional C++ですが、この項目8に依ると、コンストラクタでの例外を禁止しているわけではない。でも、ちゃんと作れと。結局コンストラクタで例外投げてもいいのかどうなのかという話。
結論から言えば、コンストラクタで例外を投げても大丈夫。ただし、その場合、リークが発生してないことをしっかり確認せよ。オブジェクトが中途半端な状態になってしまうことは問題ない。また、デストラクタでの例外はやはりNGという結論。よく読めば、Cマガの記事にも例外を投げるなと書いているわけでは無くて、投げるなら注意せよという趣旨ではある。
この辺りの仕様があやふやになっている(わけではないんだろうけど、知られていない、あるいは知らなくてもそれなりに動く)とか、結局やっぱり手動でnew, deleteするあたりにC++の限界を感じざるを得ない。Exceptionalを読めば読むほど、C++のそこの深さがわかると言うより、泥沼の底が見えなくなって、やる気なくすんですけど。


さて、もっと詳しく。
つまりこういうことだ(ろう)。newしたときの挙動は、基本的には2つのみ。かつ、そのどちらかになるように調整せよ。その2つとは、

  • 正しく実行できて、全ての初期化が行われ、内部状態も正しい(成功)
  • 何らかの処理で失敗したが、途中で確保したリソースは全て解放され、以降は扱えない状態になり、例外が投げられる(失敗)

ちゃんと仕様を読んだ方がいいんだろうが、gccで適当に実験してしまう。メンバ変数の初期化と失敗時の挙動がどうなるのかという話。

#include 

using namespace std;

class D {
public:
  static int i;
  D() { cout << "D"; if (i++ > 10) throw 0;}
  ~D() { cout << "~D"; }
};

int D::i;

class E {
public:
  E() { cout << "E"; }
  ~E() { cout << "~E"; }
};

class F {
public:
  F() { cout << "F"; }
  ~F() { cout << "~F"; }
};

class C {
public:
  C() : e(), f(), pe(new E), pd(new D[20]) {}
  ~C() { cout << "~C"; }

private:
  E e;
  F f;
  E* pe;
  D* pd;
};

int main()
{
  try {
    C c;
  } catch(...) {
    cout << "catch" << endl;
  }
}

実行結果

EFEDDDDDDDDDDDD~D~D~D~D~D~D~D~D~D~D~D~F~Ecatch

激しくわかりにくいのだが、何をしているのかというとメンバにオブジェクト(E, F)とポインタ(E)とオブジェクトの配列(Dの配列)を持つクラス(C)の初期化時例外における挙動。

  • 初期化リストを順に初期化、コンストラクタ呼び出し(E, F)
  • newもする(new E)
  • 配列確保(new D[])
  • 各要素に関してコンストラクタ(D)
  • 途中で例外(@D())
  • 途中まで初期化していたDをデストラク
  • 確保したDの配列領域も解放(らしい)
  • コンストラクタと逆順にデストラクタを呼び出し、解放(F, E)

こんなところである。配列の途中で例外が起こったら、途中まで確保したオブジェクトは全て解放されるようだ。Exceptionalによると、このとき確保したDの配列空間もただしく削除されるそうだ。さらに、ここから正しく確保されたF, Eの順で逆順にデストラクタが呼ばれる。意外とすごい。すごいがんばる。でだ。そう、お気づきの通り、newしたEは解放されない。もちろん配列にしても同じ。なのでリークする。newしたら自分でちゃんと面倒見なさいということらしい。それさえなければ、どうにかこうにか上のようにうまいこと、ちゃんと言語仕様が面倒見てくれるようになっているというわけである。
ということで、こう修正。

  C() : e(), f() {
    pe = new E();
    try {
      pd = new D[20]();
    } catch(...) {
      delete pe;
      throw;
    }
  }

では、Cマガの例はどうなるのかといえば、こういうことだろう。

try {
  mBuff2 = new char[LARGE_SIZE];
} catch (...) {
  delete[] mBuff1;
  throw;
}

こうすればfactoryを作らなくても大丈夫ですね。結局こんなことしないで、素直にvector使いなさいってことで。<< に失敗とかの話はまた別の話。
なんか変なところあったら指摘してください。でも、これくらいのことはExceptionalに書いてあるような気がするなぁ。何度読んでも身にしみない。