読者です 読者をやめる 読者になる 読者になる

ボレロ村上 - ENiyGmaA Code

中3女子です。

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 にも boost::is_integral にもマッチしてしまうからだ。
このような場合、オーバーロード解決は曖昧になってしまう。

#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_seq は、引数に型シーケンスをとる。

    • enable_switch::enable_case

条件のうち、いずれかにマッチした場合に適用される。
N は、enable_switch に渡した条件のうち、マッチすべきインデックスを示す Integral Constant 型である。
enable_case_c は、N に整数定数を渡す。
ArgN は、判定する テンプレート引数である。

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