3種類の例外安全性
関数の例外安全性には、以下の3種類がある。
- 資源解放保証((「資源解放保証」「原子性保証」「無送出保証」っていう言葉は
- bold;">いま勝手に作りました、すいません。Exceptional C++ などでは「弱い保証」「強い保証」「投げない保証」と呼ばれていますが、「強い」「弱い」が曖昧で分かりにくいのと「投げない」が漢字じゃないのが嫌だったので。)):例外が送出された場合でも、その関数呼び出しはリソースリークを起こさない。
- 原子性保証
- 例外が送出された場合、その関数呼び出しは何の変化ももたらさない。
- 無送出保証
- その関数は決して例外を送出しない。
それぞれの意味を正しく理解して、堅牢なプログラムを書けないといけない。
代入演算子を書かなければならないとき
C++ の場合、特別な理由がない限り、クラスはコピー可能にする。この辺りは、java.lang.Cloneable の位置付けとは全然違うので、Java 慣れしてる人は注意が必要。
代入演算子を宣言しなかった場合、コンパイラが自動的に生成してくれる。これは、以下のようにメンバ変数ごとの代入を行う。
C& operator = (const C& another) { // 基底クラスがある場合には、最初にそれの代入演算子が実行される。 m_1 = another.m_1; m_2 = another.m_2; // 以下、メンバ変数ごとの代入。 return *this; }
このため、以下のような場合には、専用の代入演算子を定義しなければならない。
- メンバ変数にポインタを持つ場合。
- ディープコピーではなくシャロウコピーにしたいのならば、デフォルトの代入演算子で構わない。
- 基底クラスやメンバ変数の代入演算子が例外を送出する可能性がある場合。
- これらの代入演算子のうちで最初に呼び出されるものだけは、無送出保証ではなく原子性保証に緩められる。
前者は当たり前なので忘れることは少ないし、早晩動作がおかしくなるので発見しやすい。一方、後者は忘れてしまうことが多く、例外が発生しない限りは動作がおかしくなることもないので気づきにくい。
代入演算子の定石
代入演算子を実装しなければならない場合は、以下のようにする。
class some_class { public: some_class& operator = (const some_class& another) { some_class tmp_(another); swap(tmp_); return *this; } void swap(some_class& another) throw (); };
ポイントは:
- 一時オブジェクト
tmp_
を作り、this
とtmp_
の中身を入れ替える (swap
)。 swap
は例外を決して送出しない。- 「コピー」の具体的な操作は、コピーコンストラクタに任せる。
細かな注意点としては、
- 最後は必ず
return *this;
にする。 - 返却値には
const
を付けない。
tmp_
を作るのは、メンバ変数ごとの代入を続けている途中で例外が投げられた場合に代入先が中途半端に変わった状態になってしまうのを防ぐため (例外安全性・原子性保証)。おまけとして、自己代入の場合も自動的にケアされる。
swap には無送出保証の例外安全性が必要
『代入演算子の定石』では、swap
には無送出保証の例外安全性が必要だと書いた。これに対して、「これなら原子性保証だけで十分なのでは?」という疑問が湧くかもしれない。ちなみに、こういうコード:
some_class& operator = (const some_class& another) { some_class tmp_(another); swap(tmp_); return *this; }
確かに、swap
が例外を投げても (原子性保証があれば) 問題はないように見える。
実は、swap
の無送出保証を要請しているのはこの代入演算子ではない。これは、このクラスをメンバ変数に持ったり継承したりするクラスが例外安全な swap
を持つためのものだ。
たとえば、以下のようなクラスの swap
の実装を見るとこれが分かる。
class enclosing_class { public: void swap(enclosing_class& another) { m_someValue.swap(another.m_someValue); m_anotherValue.swap(another.m_anotherValue); } private: some_class m_someValue; another_class m_anotherValue; };
この enclosing_class:swap
に原子性保証を与えるためには、some_class::swap
や another_class::swap
に無送出保証が必要になる。
まとめると: