メタ関数の実装技法 - その2
前回(メタ関数の実装技法 - その1 - ボレロ村上 - ENiyGmaA Code)に続き、メタ関数の実装技法その2です。
関数の呼び分けと decltype を利用したメタ関数
たとえば、クラスメンバに型名 value_type を持つか判定するトレイトを考える。
-
- has_value_type
template<typename T, typename U = typename T::value_type> std::true_type value_type_tester(int); template<typename T> std::false_type value_type_tester(long); template<typename T> struct has_value_type : decltype(value_type_tester<T>(0)) {};
これは value_type_tester の呼出において、T::value_type が定義されている場合は上が呼び出され、そうでない場合は下が呼び出される。
その返値型を decltype で取ることによって(true_type | false_type)メンバの有無を判定する。
なお引数が int と long になっているのは、返値型のみが異なる関数を宣言することはできないからである。
またテンプレート引数を非型にすれば、非型メンバに対するトレイトを定義することもできる。
こうしたトレイトは、しばしば has_xxx と呼ばれるものである。
本の虫: C++11におけるモダンなhas_xxxの実装
優先順のある複数条件の特殊化によってメタ関数を書く
たとえば、ある条件を満たすときの動作、そうでないが別の条件を満たすときの動作、いずれの条件も満たさない場合の動作、
……といった具合に、if-else if のように動作をフォールバックさせたいときがある。
SFINAE でこれをやりたい場合は、以下のような方法がある。
enable_switch - 複数の重複しうるコンパイル時条件で、SFINAE によるオーバーロードを書くには - ボレロ村上 - ENiyGmaA Code
またメタ関数の実装においては、部分特殊化によって同じようなことをもっと簡単にやることもできる。
たとえば、以下のような3つのコンテナクラスがあったとする。
template<typename T> struct FuckinContainer1 { typedef T value_type; value_type* begin(); value_type* end(); }; template<typename T> struct FuckinContainer2 { typedef T* iterator; iterator begin(); iterator end(); }; template<typename T> struct FuckinContainer3 { typedef T const* const_iterator; const_iterator begin(); const_iterator end(); };
さて、これらのクラスは困ったことに、value_type や iterator といったメンバ型が統一的に用意されていない。
そもそも自作コンテナをどうこうすることから疑うべきだが、もしあなたのプロジェクトにこういう不統一をするプログラマがいたら即刻窓から投げ捨てるべきである。
しかしながら現実はそう単純ではない。
その糞コードを書いた人間はすでに死んでいて直す者がいないかもしれないし、そもそも手を入れることができないライブラリの中に糞コードが入っているかもしれない。
そのような既存の糞コードや、あるいはこれから生み出されるかもしれない糞コードにも、可能な限り対応できるコードを書かなければならない時もある。
ここでは、上記の糞コンテナクラスから、なんとかして全てから value_type を取り出すようなメタ関数を考える。
-
- container_value_type
/* has_xxx のバリエーション。実装は上記参照 */ template<typename T> struct has_value_type; template<typename T> struct has_iterator; template<typename T> struct has_const_iterator; template<typename C, bool HasValueType, bool HasIterator, bool HasConstIterator> struct container_value_type_impl {}; template<typename C, bool HasIterator, bool HasConstIterator> struct container_value_type_impl<C, true/*HasValueType*/, HasIterator, HasConstIterator> { typedef typename C::value_type type; }; template<typename C, bool HasConstIterator> struct container_value_type_impl<C, false/*HasValueType*/, true/*HasIterator*/, HasConstIterator> { typedef typename std::iterator_traits<typename C::iterator>::value_type type; }; template<typename C> struct container_value_type_impl<C, false/*HasValueType*/, false/*HasIterator*/, true/*HasConstIterator*/> { typedef typename std::iterator_traits<typename C::const_iterator>::value_type type; }; template<typename C> struct container_value_type : container_value_type_impl< C, has_value_type<C>::value, has_iterator<C>::value, has_const_iterator<C>::value > {};
見てのとおり、container_value_type_impl は三段階に部分特殊化されている。
インタフェースとなる container_value_type は、各々の has_xxx による判定結果を container_value_type_impl に転送する役目である。
このような手法は、なにも糞コードへの対応に限ったものではない。
クラスのコンセプトを決めるとき、
-
- 型 xxx が定義されていること
- 型 yyy が定義されていること [Optional] (定義されていない場合は xxx から推論される)
このような形にすることで、コンセプトが要求する最小要件を小さくすることができる。
そうしたコンセプトを基にした実装において、こうした手法が必要になることがある。
(たとえば std::allocator_traits など)