More C++ Idioms/throw しない swap(Non-throwing swap)
目次 |
[編集]
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 [編集]
- ^ http://groups.google.ca/group/comp.lang.c++.moderated/browse_thread/thread/b396fedad7dcdc81 Namespace issues with specialized swap