enable_switch - 複数の重複しうるコンパイル時条件で、SFINAE によるオーバーロードを書くには
久々にプログラミングの記事を。
boost::enable_if メタ関数 (C++0x なら std::enable_if)は非常に重要なユーティリティである。
SFINAE によって、関数テンプレートやクラステンプレートの、コンパイル時条件によるオーバーロードを行う。
メタプログラミングに親しむ C++er にとっては、今さら説明の必要もないだろう。
例えば、組み込みの整数型と浮動小数点数型とで処理を分けたいならば、次のように書けばいい。
明快で簡単である。
#include <boost/type_traits.hpp> #include <boost/utility/enable_if.hpp> template<typename T> void test(T, typename boost::enable_if<boost::is_integral<T> >::type* = 0) {} template<typename T> void test(T, typename boost::enable_if<boost::is_float<T> >::type* = 0) {}
では、更に int 型についてのみ処理を分けたい場合はどう書けばよいだろうか。
#include <boost/type_traits.hpp> #include <boost/utility/enable_if.hpp> template<typename T> void test(T, typename boost::enable_if<boost::is_same<T, int> >::type* = 0) {} template<typename T> void test(T, typename boost::enable_if<boost::is_integral<T> >::type* = 0) {} template<typename T> void test(T, typename boost::enable_if<boost::is_float<T> >::type* = 0) {}
残念ながら、上記のコードは正しくない。
なぜなら、テンプレート引数 T が int であるとき、enable_if の条件式である
boost::is_same
このような場合、オーバーロード解決は曖昧になってしまう。
#include <boost/type_traits.hpp> #include <boost/utility/enable_if.hpp> #include <boost/mpl/logical.hpp> template<typename T> void test(T, typename boost::enable_if<boost::is_same<T, int> >::type* = 0) {} template<typename T> void test(T, typename boost::enable_if< boost::mpl::and_<boost::is_integral<T>, boost::mpl::not_<boost::is_same<T, int> > > >::type* = 0 ) {} template<typename T> void test(T, typename boost::enable_if<boost::is_float<T> >::type* = 0) {}
これを回避するには、以上のように条件式を書いてやればよい。
テンプレート引数 T が int であるとき、2番目のオーバーロードにはマッチしなくなるためだ。
しかし、このコードはかなり汚い。
見た目が悪いだけでなく、同じ条件式を2度書いたり無駄がある。
リファクタリングの妨げにもなるだろう。
しかも、更にオーバーロードが増える場合はどうだろう。
オーバーロード条件が重ならなければよいが、前述のように重なってしまえば、
曖昧さを排するために書くべき条件式は、雪だるま式に煩雑になるだろう。
これを解決するためには、例えば「どれかの条件にマッチしたら、それ以上オーバーロード候補を探さない」という風にすればよいだろう。
とはいえ、C++ の言語仕様にそんな仕組みはないので、単に enable_if と単純な条件式による SFINAE では不可能である。
そういうわけで書いたのが、enable_switch メタ関数である。
sprig というのは僕の俺々ライブラリであるが、下記は基本的に標準以外のライブラリは Boost しか使っていないので、コピペしてやれば問題なく動くだろう。
VC9 + Boost 1.46 で動作を確認している。
-
- sprig/enable_switch.hpp
#ifndef SPRIG_ENABLE_SWITCH_HPP #define SPRIG_ENABLE_SWITCH_HPP #include <boost/utility/enable_if.hpp> #include <boost/type_traits/is_same.hpp> #include <boost/mpl/vector.hpp> #include <boost/mpl/size.hpp> #include <boost/mpl/void.hpp> #include <boost/mpl/long.hpp> #include <boost/mpl/bool.hpp> #include <boost/mpl/find_if.hpp> #include <boost/mpl/if.hpp> #include <boost/mpl/remove.hpp> #include <boost/mpl/apply.hpp> #include <boost/mpl/unpack_args.hpp> #include <boost/preprocessor/repetition/enum_params.hpp> #include <boost/preprocessor/repetition/enum_params_with_a_default.hpp> // // SPRIG_ENABLE_SWITCH_CASE_MAX_ARITY // SPRIG_ENABLE_SWITCH_ARGS_MAX_ARITY // #ifndef SPRIG_ENABLE_SWITCH_CASE_MAX_ARITY # define SPRIG_ENABLE_SWITCH_CASE_MAX_ARITY 16 #endif // #ifndef SPRIG_ENABLE_SWITCH_CASE_MAX_ARITY #ifndef SPRIG_ENABLE_SWITCH_ARGS_MAX_ARITY # define SPRIG_ENABLE_SWITCH_ARGS_MAX_ARITY 16 #endif // #ifndef SPRIG_ENABLE_SWITCH_ARGS_MAX_ARITY namespace sprig { // // case_match // template<long N> struct case_match : public boost::mpl::long_<N> {}; // // default_match // struct default_match : public boost::mpl::long_<-1> {}; namespace enable_switch_detail { template<typename Seq> struct remove_void : boost::mpl::remove<Seq, boost::mpl::void_> {}; // // enable_switch_impl // # define SPRIG_ENABLE_SWITCH_ENUM_ARGS_T() \ BOOST_PP_ENUM_PARAMS_WITH_A_DEFAULT(SPRIG_ENABLE_SWITCH_ARGS_MAX_ARITY, typename Arg, boost::mpl::void_) # define SPRIG_ENABLE_SWITCH_ENUM_ARGS() \ BOOST_PP_ENUM_PARAMS(SPRIG_ENABLE_SWITCH_ARGS_MAX_ARITY, Arg) template<typename CaseSeq> struct enable_switch_impl { public: typedef CaseSeq args_type; private: template<SPRIG_ENABLE_SWITCH_ENUM_ARGS_T()> struct apply_match { template<typename Pred> struct apply : public boost::mpl::apply< boost::mpl::unpack_args<Pred>, typename enable_switch_detail::remove_void<boost::mpl::vector<SPRIG_ENABLE_SWITCH_ENUM_ARGS()> >::type > {}; }; template<SPRIG_ENABLE_SWITCH_ENUM_ARGS_T()> struct match_pos : public boost::mpl::find_if< args_type, apply_match<SPRIG_ENABLE_SWITCH_ENUM_ARGS()> >::type::pos {}; template<typename Pos> struct pos_to_result : public boost::mpl::if_c< (Pos::value != boost::mpl::size<args_type>::value), case_match<Pos::value>, default_match >::type {}; public: // // match // template<SPRIG_ENABLE_SWITCH_ENUM_ARGS_T()> struct match : public pos_to_result< match_pos<SPRIG_ENABLE_SWITCH_ENUM_ARGS()> >::type {}; // // case_c // case_ // default_ // template<long N, SPRIG_ENABLE_SWITCH_ENUM_ARGS_T()> struct case_c : public boost::mpl::bool_<match<SPRIG_ENABLE_SWITCH_ENUM_ARGS()>::value == N> {}; template<typename N, SPRIG_ENABLE_SWITCH_ENUM_ARGS_T()> struct case_ : public case_c<N::value, SPRIG_ENABLE_SWITCH_ENUM_ARGS()>::type {}; template<SPRIG_ENABLE_SWITCH_ENUM_ARGS_T()> struct default_ : public boost::mpl::bool_<match<SPRIG_ENABLE_SWITCH_ENUM_ARGS()>::value == -1> {}; // // enable_case_c // enable_case // enable_default // template<long N, SPRIG_ENABLE_SWITCH_ENUM_ARGS_T()> struct enable_case_c : public boost::enable_if< case_c<N, SPRIG_ENABLE_SWITCH_ENUM_ARGS()> > {}; template<typename N, SPRIG_ENABLE_SWITCH_ENUM_ARGS_T()> struct enable_case : public boost::enable_if< case_<N, SPRIG_ENABLE_SWITCH_ENUM_ARGS()> > {}; template<SPRIG_ENABLE_SWITCH_ENUM_ARGS_T()> struct enable_default : public boost::enable_if< default_<SPRIG_ENABLE_SWITCH_ENUM_ARGS()> > {}; // // enable_case_ret_c // enable_case_ret // enable_default_ret // template<long N, typename Ret = void, SPRIG_ENABLE_SWITCH_ENUM_ARGS_T()> struct enable_case_ret_c : public boost::enable_if< case_c<N, SPRIG_ENABLE_SWITCH_ENUM_ARGS()>, Ret > {}; template<typename N, typename Ret = void, SPRIG_ENABLE_SWITCH_ENUM_ARGS_T()> struct enable_case_ret : public boost::enable_if< case_<N, SPRIG_ENABLE_SWITCH_ENUM_ARGS()>, Ret > {}; template<typename Ret = void, SPRIG_ENABLE_SWITCH_ENUM_ARGS_T()> struct enable_default_ret : public boost::enable_if< default_<SPRIG_ENABLE_SWITCH_ENUM_ARGS()>, Ret > {}; }; # undef SPRIG_ENABLE_SWITCH_ENUM_ARGS_T # undef SPRIG_ENABLE_SWITCH_ENUM_ARGS } // namespace enable_switch_detail // // enable_switch_seq // template<typename CaseSeq> struct enable_switch_seq : public enable_switch_detail::enable_switch_impl<CaseSeq> { public: typedef enable_switch_seq type; }; // // enable_switch // template<BOOST_PP_ENUM_PARAMS_WITH_A_DEFAULT(SPRIG_ENABLE_SWITCH_CASE_MAX_ARITY, typename Case, boost::mpl::void_)> struct enable_switch : public enable_switch_detail::enable_switch_impl< typename enable_switch_detail::remove_void<boost::mpl::vector<BOOST_PP_ENUM_PARAMS(SPRIG_ENABLE_SWITCH_CASE_MAX_ARITY, Case)> >::type > { public: typedef enable_switch type; }; } // namespace sprig #endif // #ifndef SPRIG_ENABLE_SWITCH_HPP
可変長引数テンプレートをエミュレートするため Boost.Preprocessor を使っている部分が煩雑だが、読めないことはないと思う。
機能のみざっくり説明してみる。
まずは、簡単なテストコードを示す。
#include <typeinfo> #include <boost/type_traits.hpp> #include <boost/mpl/quote.hpp> #include <boost/mpl/lambda.hpp> #include <sprig/enable_switch.hpp> typedef sprig::enable_switch< boost::mpl::lambda<boost::is_same<boost::mpl::_1, int> >::type, boost::mpl::quote1<boost::is_integral>, boost::mpl::quote1<boost::is_float> > switch_type; template<typename T> void switch_test(T, typename switch_type::enable_case_c<0, T>::type* = 0) { std::cout << typeid(T).name() << " -> int" << std::endl; }; template<typename T> void switch_test(T, typename switch_type::enable_case_c<1, T>::type* = 0) { std::cout << typeid(T).name() << " -> is_integral" << std::endl; }; template<typename T> void switch_test(T, typename switch_type::enable_case_c<2, T>::type* = 0) { std::cout << typeid(T).name() << " -> is_float" << std::endl; }; template<typename T> void switch_test(T, typename switch_type::enable_default<T>::type* = 0) { std::cout << typeid(T).name() << " -> default" << std::endl; }; int main() { switch_test(0); switch_test(long(0)); switch_test(0.0); switch_test("hoge"); return 0; }
-
- 出力結果(VC9)
int -> int long -> is_integral double -> is_float char const * -> default
int は最初の条件にマッチする。
is_integral にもマッチするが、先に書かれたほうが適用される。
-
- enable_switch
- enable_switch
条件判定のためのメタ関数クラスを列挙する。
メタ関数クラスは、enable_switch に渡した順通りに評価される。
enable_switch_seq は、引数に型シーケンスをとる。
-
- enable_switch::enable_case
- enable_switch::enable_case
条件のうち、いずれかにマッチした場合に適用される。
N は、enable_switch に渡した条件のうち、マッチすべきインデックスを示す Integral Constant 型である。
enable_case_c は、N に整数定数を渡す。
ArgN は、判定する テンプレート引数である。
-
- enable_switch::enable_default
- enable_switch::enable_default
条件のうち、いずれにもマッチしなかった場合に適用される。
また、以下のように書いても同じ動作になる。
慣れ親しんだ enable_if に渡して使う場合である。
#include <typeinfo> #include <boost/utility/enable_if.hpp> #include <boost/type_traits.hpp> #include <boost/mpl/quote.hpp> #include <boost/mpl/lambda.hpp> #include <sprig/enable_switch.hpp> typedef sprig::enable_switch< boost::mpl::lambda<boost::is_same<boost::mpl::_1, int> >::type, boost::mpl::quote1<boost::is_integral>, boost::mpl::quote1<boost::is_float> > switch_type; template<typename T> void switch_test(T, typename boost::enable_if<switch_type::case_c<0, T> >::type* = 0) { std::cout << typeid(T).name() << " -> int" << std::endl; }; template<typename T> void switch_test(T, typename boost::enable_if<switch_type::case_c<1, T> >::type* = 0) { std::cout << typeid(T).name() << " -> is_integral" << std::endl; }; template<typename T> void switch_test(T, typename boost::enable_if<switch_type::case_c<2, T> >::type* = 0) { std::cout << typeid(T).name() << " -> is_float" << std::endl; }; template<typename T> void switch_test(T, typename boost::enable_if<switch_type::default_<T> >::type* = 0) { std::cout << typeid(T).name() << " -> default" << std::endl; }; int main() { switch_test(0); switch_test(long(0)); switch_test(0.0); switch_test("hoge"); return 0; }