Effective C++勉強メモ: Item 25 swap関数あれこれ
C++にはpimplというイディオムがある。
何か大きなデータを持つオブジェクトがある場合、単純にデータメンバとしてしまうと(例えば関数にデータを渡したりする時に)コピーが発生するとコストが高い。
そういう場合には、データを実際に保持するクラスと、そのデータのインタフェースとなるクラスをわけると便利なことが多い。インタフェースクラスがデータクラスにポインタを持つ形になる。Pointer-to-an-Implementationでpimplらしい。
class ObjImplementation { private: vector<double> v; }; class Obj { private: ObjImplementation* pimpl; };
ObjがObjImplementationへのポインタを持っている。
さて、このようなクラスがswapされる場合どうなるか。
std内のswapの実装は:
namespace std { template <typename T> void swap(T& a, T& b) { T temp(a); a = b; b = temp; } }
となっており、コピーコンストラクタが呼ばれている。pimplイディオムの場合、インタフェースクラスのコピーコンストラクタは大抵データクラス自体もコピーするようになっているので、このswap
は大変コストが高い。実際には二つのインタフェースクラスのオブジェクトの保持するポインタを入れ替えるだけでいい。そのために自前のswap関数を定義する。
class Obj { public: void swap(Obj& b) { using std::swap; swap(pimp, b.pimpl); } private: ObjImplementation* pimpl; }; namespace std { template<> void swap<Obj>(Obj& a, Obj& b) { a.swap(b); } }
pimpl
はprivate
なのでアクセスするためにObj
クラス内でswap
メンバ関数を(std::swap<*ObjImplementation>
を使って)定義する。その上でstd
名前空間内でtemplate specializationしてstd::swap<Obj>
を定義する。std::swap
は様々な場面で使われるので自前のより効率のいい定義をしておくことには大いに意味がある。
std
内に新しいクラスや関数を定義するのは許されていない。そのようなコードはコンパイルするが、動作は未定義となる。例外がすでにstd
内にあるテンプレートの特殊化で、上記の例はまさにそのケースだ。
Obj
がテンプレートクラスだとさらにややこしくなる。
というのも
namespace std { template<T> void swap<Obj<T> >(Obj<T>& a, Obj<T>& b) { a.swap(b); } }
としたくなるが、これはstd::swap
のPartial Specializationになる(Obj型は確定したがまだObj自体の型パラメータになるTが未決定)。関数はPartial Specializationができないので上記のコードはコンパイルエラー。
テンプレート関数はPartial Specializationではなく、Overloadingするのが一般的には正しい:
namespace std { template<T> void swap(Obj<T>& a, Obj<T>& b) { a.swap(b); } }
これは既存のswapテンプレートをspecializeするのではなく、関数をoverloadしている。が、std
に新しい関数を足すのは動作が未定義になる。コンパイルしてしまう分、こちらのほうが危険度は高い。
Obj
がクラステンプレートの場合、最善なやりかたは
namespace objSpace { class Obj { public: void swap(Obj& b) { using std::swap; swap(pimp, b.pimpl); } private: ObjImplementation* pimpl; }; template<T> void swap(Obj<T>& a, Obj<T>& b) { a.swap(b); } }
とObj<T>
をとるswap
をObj
クラスの定義と同じ名前空間にいれておくこと。これで
using std::swap;
swap(obj1, obj2);
などと別の場所からObj
インスタンスに対してswap
が使われた時に、Obj
の名前空間にswap
があればまずそれが適用される(上記のようにusing std::swap
と指定してあっても)。
std::swap(obj1, obj2);
とすると問答無用でstd::swap
が使われるので注意。一般的にはswap
を使う側の心がけとしてusing std::swap; swap(o1, o2);
として、別のところでクラス特有のOverloadingがあったらそれを使えるようにするべき。
最後にもうひとつ大事な点。swap
は例外安全のために多用される。メンバ関数のswap
は例外を投げないよう設計しよう。