コンストラクタの例外安全性とスマートポインタ

コンストラクタ内で例外を投げるべきではない、という主張がある。(今回は、デストラクタ内でという話ではない。念のため。)
例外を投げる可能性のある処理をコンストラクタから追い出すのが難しいケースなんて普通はそれほど無いので、基本的には素直にそうしておくとよい。

さて不幸にも実際にコンストラクタ内で例外が投げられると何が起こるのか。以下のコードを見るとわかるように、実は言語的にはちょっと賢いことをしている。

#include <iostream>
#include <memory>
using namespace std;

struct A {
  explicit A(const char* name): name_(name) {
    cout << "A: constructed: " << name_ << endl;
  }
  ~A() { cout << "A: destructed: " << name_ << endl; }
  const char* name_;
};

struct B {
  B() { throw "Boooom!!!"; cout << "B: constructed" << endl; }
  ~B() { cout << "B: destructed" << endl; }
};

struct X {
  X():
      a0_("direct"),
      a1_(new A("raw ptr")),
      a2_(new A("auto_ptr")),
      b_(new B),
      a3_(new A("auto_ptr never constructed")) {
    cout << "X: constructed" << endl;
  }
  ~X() { delete a1_; cout << "X: destructed" << endl; }

  A a0_;
  A* a1_;
  auto_ptr<A> a2_;
  auto_ptr<B> b_;
  auto_ptr<A> a3_;
};

int main() {
  try {
    X x;
    cout << "Construction completed" << endl;
  } catch (...) {
    cout << "Recovered" << endl;
  }
}

実行結果:

A: constructed: direct
A: constructed: raw ptr
A: constructed: auto_ptr
A: destructed: auto_ptr
A: destructed: direct
Recovered

コンストラクタはメンバ変数を宣言順に初期化し、最後に初期化ブロックを実行するのだが、途中で例外が起こると、デストラクタのコードブロックは実行されず、初期化が完了した変数だけが逆順にデストラクトされる。

つまり、初期化リストで new してデストラクタで明示的に delete を呼ぶようなコードではリークする可能性がある。しかしスマートポインタに突っ込んでおけば安心安全ぐっすり眠れてしあわせである。スマートポインタが使えない宗教の人は、初期化リストではなく、コンストラクタのコードブロック内で new と代入をするほうが C++ 的には正しい。

余談だが、あるクラスが "所有する" オブジェクトを直接メンバ変数として持つ代わりにポインタにしておくことのメリットはいくつかある。オブジェクトサイズが減るのでコピーのコストが下がると同時にローカル変数でもそのメンバ変数の領域がスタックではなくヒープに確保されるだとか、前方宣言だけあれば良いのでヘッダを include する際に依存関係を増やさずにすむだとか。dereference のオーバーヘッドとか普通は気にしない。