Arantium Maestum

プログラミング、囲碁、読書の話題

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);
  }
}

pimplprivateなのでアクセスするために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>をとるswapObjクラスの定義と同じ名前空間にいれておくこと。これで

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は例外を投げないよう設計しよう。