More C++ Idioms/throw しない swap(Non-throwing swap)

出典: フリー教科書『ウィキブックス(Wikibooks)』

throw しない swap(Non-throwing swap)
[編集]

意図[編集]

  • 例外安全で効率的な swap(交換)操作を実装する
  • その swap 操作に対して、総称プログラミングを有効活用できるよう統一されたインタフェースを提供する。

別名[編集]

  • 例外安全な swap(Exception safe swap)

動機[編集]

swap の典型的な実装は次のようになる。

void swap (T &a, T &b)
{
  T temp (a);
  a = b;
  b = temp;
}

同じ型の大きく複雑なオブジェクトの swap は、中間的な一時オブジェクトのためのリソースの獲得と解放により、 非常に非効率になりうる。 また、前述の swap の実装では、リソースが利用可能ではなかった場合、例外が送出されるかもしれない。 そもそも新しいリソースの要求が必要でなかった場合には、そのような挙動は認めがたい。

解法とサンプルコード[編集]

throw しない swap イディオム(Non-throwing swap idiom)は、望ましい効果を得るためにハンドル・ボディ(Handle Body)イディオムを使用する。 対象の概念は、2つの実装クラスに分割される。 一つは、ハンドル(handle)であり、もう一つはボディ(body、本体)である。 ハンドルはボディ(本体)オブジェクトへのポインタを保持する。 swap は、単純なポインタの交換として実装される。 例外を送出しないことが保証され、新しいリソースが獲得も解放もされないため非常に効率的である。

namespace Orange {
class String 
{
    char * str;
  public:
    void swap (String &s) throw ()
    {
      std::swap (this->str, s.str);
    }
};
}

効率的で例外安全な swap 関数は、(上述のように)メンバ関数として実装可能だが、 throw しない swap イディオムは、単純さ、整合性、そして総称プログラミングの有効活用のために その先を行く。 std::swap の明示的特殊化を、クラス自身の名前空間だけでなく std 名前空間にも追加すべきである。

namespace Orange { // String の名前空間
  void swap (String & s1, String & s2) throw ()
  {
    s1.swap (s2);
  }
}
namespace std {
  template <>
  void swap (Orange::String & s1, Orange::String & s2) throw ()
  {
    s1.swap (s2);
  }
}

これら 2 箇所への追加は、swap の 2 つの異なる一般的な使用法に対応する。 1 つは、被修飾の swap であり、もう一つは完全修飾の swap (例えば std::swap)である。 非修飾の swap が使用される時には、正しい swap が Koenig の名前探索により(既に定義済みのものから)発見される。 完全修飾の swap が使用される時には、Koenig の名前探索は抑止され、 std 名前空間内のものが代わりに使用される。 これはよく知られた挙動である。 残りの議論では、完全修飾の swap のみを用いる。 C++ プログラマはしばしば下記のように swap 関数を慣用的に std:: で完全修飾して呼び出すため、統一された見た目と印象を与える。

template <class T>
void zoo (T t1, T t2) {
//...
int i1, i2;
std::swap(i1,i2); // 統一性に注目
std::swap(t1,t2); // 同上
}

そのような場合、zoo が先に定義した String によってインスタンス化されたときに、正しい効率的な swap の実装が選択される。 さもなくば、 メンバ関数の swap と、名前空間での swap 関数を定義した目的を完全に無にして デフォルトの std::swap 関数テンプレートがインスタンス化されるだろう。 swap の明示的特殊化を std 名前空間に定義するというこのイディオムは、特に総称プログラミングで有用である。

throw しない swap イディオムの使用による統一性は、下記の例のような再帰的な利用をもたらす。

class UserDefined 
{
    String str;
  public:
    void swap (UserDefined & u) throw () 
    { 
      std::swap (str, u.str); 
    }
};
namespace std
{
  // std 名前空間のテンプレートの完全特殊化は、std 名前空間に追加できる
  template <>
  void swap (UserDefined & u1, UserDefined & u2) throw ()
  {
    u1.swap (u2);
  }
}
class Myclass
{
    UserDefined u;
    char * name;
  public:
    void swap (Myclass & m) throw ()
    {
      std::swap (u, m.u);       // 統一性によるイディオムの再帰的な使用
      std::swap (name, m.name); // 同上
    }   
}
namespace std
{
  // std 名前空間のテンプレートの完全特殊化は、std 名前空間に追加できる
   template <> 
   void swap (Myclass & m1, Myclass & m2) throw ()
   {
     m1.swap (m2);
   }
};

従って、例外安全で効率的な swap の実装に従った、その型の std::swap の特殊化を定義するのは良い考えである。 現在の C++ 標準は、新しいテンプレートを std 名前空間に追加することを認めていないが、std 名前空間のテンプレートを特殊化して std 名前空間に置くことは認めている。

警告[編集]

テンプレートクラス(例:Matrix<T>)に対して throw しない swap イディオムを使用することは微妙な問題になりうる。 C++98 標準では、ユーザ定義型に対しては std::swap の完全特殊化のみ std 名前空間に定義することを認めている。 部分特殊化や、関数オーバーロードは認められていない。 テンプレートクラス(例:Matrix<T>)に対して同様の効果を得ようとする試みは、std 名前空間での std::swap のオーバーロードに帰着するが、これは規格的には未定義動作となる。 compl.lang.c++.moderated ニュースグループ の壮大で長大な議論のスレッド [1] で指摘している人々がいるように、 これは必ずしも理想の状態ではない。 解法は、クラスが定義されている名前空間と同じ名前空間内にオーバーロードされた swap 関数テンプレートを定義するか、 何も起こらないことを期待して未定義動作を無視するか、 次期標準での修正を待つかである。

既知の利用[編集]

Boost のスマートポインタの全て (例 boost::shared_ptr)

関連するイディオム[編集]

References[編集]

  1. ^ http://groups.google.ca/group/comp.lang.c++.moderated/browse_thread/thread/b396fedad7dcdc81 Namespace issues with specialized swap