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_ を作り、thistmp_ の中身を入れ替える (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::swapanother_class::swap に無送出保証が必要になる。
まとめると:

  • 代入演算子は、原子性保証の例外安全性を備えるべき。
  • そのような代入演算子の実装には、(少なくとも原子性保証の) 例外安全性を持つ swap が必要になる。
  • 原子性保証の例外安全性を備えた swap は、基底クラスやメンバ変数が無送出保証の例外安全性を備えた swap を提供していなければ実装できない。
  • よって、いずれのクラスにあっても、swap は例外を送出してはならない。