C++ Paranoia - 汎用プログラミングにおけるTemplate MethodパターンとswapとADLフックと

汎用プログラミングにおけるTemplate Methodパターン

Aという処理とBという処理がある.これらの処理中には共通したサブルーチンXがある.この共通した処理をまとめて関数なりサブクラスが実装すべきメンバ関数とするのは非常に良くあるパターンだ.デザインパターンの言葉では Template Method と呼ばれる.また,この共通したサブルーチンXはしばしばフック(hook)と呼ばれる.
具体例としてSTLアルゴリズム,reverseとsortを考えてみると分かりやすい.reverseは最初の要素と最後の要素を交換して,最初から2番目と最後から2番目の要素を交換して・・・という実装が考えられる.sortはクイックソートにしろ何にしろ2つの変数の値を交換するという操作が必要になるのは周知のとおりである.もちろん値の交換という操作が重要になるのはこの2つのアルゴリズムだけではない.C++標準にあるアルゴリズムにおいて値の交換が必要となるものは以下のように非常に多い.

  • swap_ranges
  • iter_swap
  • reverse
  • rotate
  • random_shuffle
  • partition
  • stable_partition
  • sort
  • stable_sort
  • partial_sort
  • partial_sort_copy
  • nth_element
  • inplace_merge
  • pop_heap
  • sort_heap
  • next_permutation
  • prev_permutation

そのため*1,標準ヘッダーには2つの変数の値を交換するテンプレート関数swapが定義されている.

namespace std{

template<class T>
void (T &x, T &y)
{
  T tmp(x);
  x = y;
  y = tmp;
}

}

上に列挙したアルゴリズムにおいて,このswapは値を交換するという操作を一手に引き受けるフックであり,これら各アルゴリズムとswapの関係は汎用プログラミングの文脈におけるTemplate Methodパターンを構成していると言えるだろう.

汎用プログラミングではフックを非メンバ関数で提供しよう

通常,Template Methodパターンの実装は
一方,汎用プログラミングの文脈では,動的な多相性が必要ない場合にはswapのようなフックを非メンバ関数として提供するという方針が推奨される.
「非侵入的(non-intrusive)である」ということに尽きると思われる.

  • 組み込み型に対しても適用できる

-

swapを特殊化する

  • std::swapをあなたの書いたクラスに対して特殊化した場合 - その特殊化したstd::swapは標準ライブラリのアルゴリズムから必ず呼ばれる
  • あなたの書いたクラスに対してカスタマイズしたswapをそのクラスと同じ名前空間の非メンバ関数として定義した場合 - そのカスタマイズしたswapが標準ライブラリのアルゴリズムから呼ばれるかどうかはライブラリの実装しだいである

一見すると前者の解決方法で良いように思われる.先ほどの例ならば

#include <algorithm> // C::swapでstd::swapを使うために必要になると同時に,
                     // Cに対する特殊化のためにプライマリテンプレートを
                     // 導入するという意味も持つ

namespace my_space{

class C
{
  .....
public:
  void swap(C &x)
  {
    std::swap(p_, x.p_);
  }

private:
  int *p_;
};

}

namespace std{

template<>
void swap(my_space::my_class &x, my_space::my_class &y)
{
  x.swap(y);
}

}

とするだけで,標準ライブラリの各アルゴリズムからmy_classに対して特殊化した効率的で例外安全なswapが使われるかも知れない.なぜ「かも知れない」なのか?実は現在の標準規格(ISO/IEC 14882:2003)において,その実装でswapを呼ぶことを明言しているのはstd::reverseだけなのである.他のアルゴリズム,例えばsortなどはその実装としてそもそもswapを使うことを強制されていない.そのため,my_classに対して特殊化した効率的かつ例外安全なstd::swapがstd::sort中で一切呼ばれないこともありうる.実際,GCCのstd::sortはその実装中でstd::iter_swapを呼ぶが,そのstd::iter_swapがswapを用いることなくコピーコンストラクタとoperator=を用いて実装されているため,GCCのstd::sortがmy_class用のstd::swapを呼ぶことは一切ない.現在の標準規格の文言では,std::swapのcustomization pointとしての地位の保証が弱すぎるというわけである.
std::swapを特殊化するという方法にはもうひとつ大きな問題がある.先ほどのmy_classがクラステンプレートである場合を考えてみる.

namespace my_space{

template<class T>
class my_class
{
  .....
public:
  void swap(my_class &x)
  {
    std::swap(p_, x.p_);
  }

private:
  T *p_;
};

}

ここで「関数テンプレートは部分特殊化できない」という言語規格が大きく立ちはだかる.つまり以下のようにstd::swapを特殊化することはできないわけである.

namespace std{

template<class T>                                       // コンパイルエラー!!
void swap<my_class<T> >(my_class<T> &x, my_class<T> &y) // 関数テンプレートは
{                                                       // 部分特殊化できない!!
  x.swap(y);
}

}

一つだけ,良く勘違いされるので注意しておきたい.以下は関数の部分特殊化ではなく関数のオーバーロードである.

namespace std{

template<class T>
void swap(my_class<T> &x, my_class<T> &y)
{
  x.swap(y);
}

}

このようにstd名前空間に関数のオーバーロードを追加することは言語規格で禁止されているため,これはご法度である.
*2
*3

意図しない名前の参照

以上とはまったく逆の問題もある.
ADLによって意図しない名前が参照されてしまうのである.

swapに対する標準の方向性

ADL hooks

オリジナルの名前空間の関数テンプレートを特殊化する

前述のとおり,クラステンプレートに対して

オーバーロードをオリジナルの名前空間に追加する
ユーザ定義クラス(テンプレート)がある名前空間に関数テンプレートのオーバーロードを追加する
// ライブラリヘッダ
namespace lib{

template<class T>
void hook(T const &x)
{
  (デフォルトのhookの実装)
}

template<class T>
void algorithm(T const &x)
{
  .....
  hook(x);
  .....
}

} // namespace lib
// ユーザ定義クラステンプレートの定義ヘッダ
namespace MySpace{

template<class T>
class MyClass
{
  .....
};

template<class T>
void hook(T const &x)
{
  (MyClass用の特殊化したhookの実装)
}

} // namespace MySpace
ユーザ定義クラス(テンプレート)がある名前空間に特別に用意したcustomization用関数(テンプレート)に委譲する
// ライブラリヘッダ
namespace lib{

template<class T>
void lib_hook(T const &x)
{
  (デフォルトのhookの実装)
}

template<class T>
void hook(T const &x)
{
  lib_hook(x);
}

template<class T>
void algorithm(T const &x)
{
  hook(x);
}
// ユーザクラスの定義ヘッダ
namespace MySpace{

template<class T>
class MyClass
{
  .....
};

template<class T>
void lib_hook(MyClass<T> const &x)
{
  (MyClass用に特殊化した実装)
}

} // namespace MySpace
ライブラリはhookの機能をポリシークラステンプレートに委譲し,ユーザはそのポリシークラステンプレートの(部分)特殊化でcustomizationを実現する
// ライブラリヘッダ
namespace lib{

template<class T>
struct hook_policy
{
  static void hook_impl(T const &x)
  {
    (デフォルトのhookの実装)
  }
};

template<class T>
void hook(T const &x)
{
  hook_policy<T>::hook_impl(x);
}

} // namespace lib
// ユーザクラスの定義ヘッダ
namespace MySpace{

template<class T>
class MyClass
{
  .....
};

}

namespace lib{

template<class T>
struct hook_policy< MySpace::MyClass<T> >
{
  static void hook_impl(MySpace::MyClass<T> const &x)
  {
    (MyClass用に特殊化したhookの実装)
  }
};

*1:後で分かるがこの「そのため」という言葉は半分ウソである

*2:テンプレートの宣言順序に関する複雑な問題は2-phase lookupと呼ばれる機構に起因する.これはテンプレートを宣言した場所(Point of Definition, PoD)とテンプレートをインスタンス化した場所(Point of Instantiation, PoI)の2段階で名前の解決が行われることを指す

*3:正確な考察を行っていないため明言はできないものの,"Modern C++ Design"で関数テンプレートの部分特殊化をエミュレートするために導入しているtype2typeの手法でも,同様にこの手の非常に煩雑な仕様に翻弄される危険があるため推奨されないと思われる