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