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

凹みTips

C++、JavaScript、Unity、ガジェット等の Tips について雑多に書いています。

型の名前を静的に調べる

C++

はじめに

正直,何の役にも立たない内容ですが,Boost.Preprocessor の凄さは分かると思います.

イントロ

型を調べる!と言ったら RTTI(Run-Time Type Information: 実行時型情報)ですね.RTTIを得るためには typeid 演算子を使えば OK です.

#include <iostream>
#include <vector>

int main()
{
	std::vector<int> v;
	std::cout << typeid(v).name() << std::endl;
	return 0;
}

// 出力: class std::vector<int,class std::allocator<int> >

これをテンプレートの特殊化で(それっぽく)再現してみたいと思います.つまり typeid 演算子では動的に型を取得していることに対し,静的に型を取得してみるわけです.

テンプレートの特殊化

まず,大元となるテンプレートクラスを次のように宣言します.

template <typename T>
struct Type
{
	static std::string Get() {
		return "undefined";
	}
};

template <typename T>
std::string GetTypeName(const T& x)
{
	return Type<T>::Get();
}

この段階では,次のように表示されます.

int n;
std::cout << GetTypeName(n) << std::endl;
// 出力: undefined

ここで「int」と出力させるために,次のようにテンプレートの特殊化を行います.

template<>
struct Type<int>
{
	static std::string Get() {
		return "int";
	}
};

これで,先程のコードでintを吐き出すようになります.

int n;
std::cout << GetTypeName(n) << std::endl;
// 出力: int

理論上は全ての型について特殊化を行えばどんな型でも吐き出してくれるようになりますね!
ただ,そんな事は無理なのでもうちょっと頑張ってみます.

まず次のように「int」という入力に対して「int」と吐き出す(先ほどと同じ)コードを生成し,加えてテンプレートの部分特殊化でポインタ型に関しても「int*」のように吐き出してくれるコードも生成するマクロを定義します.

// 組込み型やテンプレートクラス以外
#define HECOMI_GETTYPE_BASE_TYPE(type)\
	template <>\
	struct Type<type>\
	{\
		static std::string Get() {\
		return #type;\
		}\
	};\
	template <>\
	struct Type<type*>\
	{\
		static std::string Get() {\
		return #type"*";\
		}\
	};

HECOMI_GETTYPE_BASE_TYPE(int)
HECOMI_GETTYPE_BASE_TYPE(float)
HECOMI_GETTYPE_BASE_TYPE(double)
HECOMI_GETTYPE_BASE_TYPE(std::string)

これで,色んな型が出力されるようになります.

int i;
float f;
double d;
std::string s;
std::cout << GetTypeName(i) << std::endl; // int
std::cout << GetTypeName(f) << std::endl; // float
std::cout << GetTypeName(d) << std::endl; // double
std::cout << GetTypeName(s) << std::endl; // std::string

しかし最初の例で見たように std::vector のようなコンテナを扱おうとすると,

HECOMI_GETTYPE_BASE_TYPE(std::vector<int>)
HECOMI_GETTYPE_BASE_TYPE(std::vector<double>)
...

なんて書いていられませんね.そこで部分特殊化で std::vector も教え込んであげます.

template <typename T>
struct Type<std::vector<T> >
{
	static std::string Get() {
		return "std::vector<" + Type<T>::Get() + ">";
	}
};

これで std::vector についてはOKになりました.

std::vector<int> v;
std::cout << GetTypeName(v) << std::endl;
// 出力: std::vector<int>

じゃぁ,2つテンプレート引数を取る std::map とかはどうなるの?ってお話になると思います.取り敢えず同じように部分特殊化してみます.

template <typename T0, typename T1>
struct Type<std::map<T0, T1> >
{
	static std::string Get() {
		return "std::map<" + Type<T0>::Get() + ", " + Type<T1>::Get() + ">";
	}
};
std::map<int, std::string> m;
std::cout << GetTypeName(m) << std::endl;
// 出力: std::map<int, std::string>

ここで std::vector と std::map についてのコードを見てみると何やら引数が変わっているだけで似ていることに気づきます.そこで,これらについてもうまい具合にマクロで定義して自動生成出来ないか,と考えるときにBoost.Preprocessor が活躍するわけです(漸く出てきました).

Boost.Preprocessorの出番!

コードを書いてしまいます.

#define HECOMI_GETTYPE_CONTAINER_INNER_TYPE(z, n, max)\
	Type<BOOST_PP_CAT(T, n)>::Get()\
	BOOST_PP_IF(BOOST_PP_EQUAL(n, BOOST_PP_SUB(max, 1)), + , + ", " +)

#define HECOMI_GETTYPE_CONTAINER_TYPE(type, n)\
	template <BOOST_PP_ENUM_PARAMS(n, typename T)>\
	struct Type<type<BOOST_PP_ENUM_PARAMS(n, T)> >\
	{\
		static std::string Get() {\
			return #type"<" + BOOST_PP_REPEAT(n, HECOMI_GETTYPE_CONTAINER_INNER_TYPE, n) + ">";\
		}\
	};

んんん,という感じだと思いますので順に説明していきます.
まず,

#define HECOMI_GETTYPE_CONTAINER_TYPE(type, n)\

ですが,このマクロの引数 type には型の名前(例:std::map),nにはテンプレート引数の数(例:2)を与えます.この引数だけで先ほどの std::vector と std::map に対する特殊化のコードを作れるわけですね.逆に言えばこれしか必要ないわけです.
次の行を見てみます.

	template <BOOST_PP_ENUM_PARAMS(n, typename T)>\
	struct Type<type<BOOST_PP_ENUM_PARAMS(n, T)> >\

ここで使用している BOOST_PP_ENUM_PARAMS は第1引数に展開する数を,第2引数に接頭辞を書きます.つまり,これは次のように展開されるわけです.

	// Ex. n = 2
	template <typename T0, typename T1>\
	struct Type<type<T0, T1> >\

そして Get() の中身を見てみると,

return #type"<" + BOOST_PP_REPEAT(n, HECOMI_GETTYPE_CONTAINER_INNER_TYPE, n) + ">";\

なっています.BOOST_PP_REPEATは,第1引数に繰り返し回数,第2引数に3引数を取る繰り返したいマクロ,第3引数に第2引数のマクロに渡す文字を入れることで,第2引数で与えたマクロを繰り返し実行します.そこで第2引数のマクロ HECOMI_GETTYPE_CONTAINER_INNER_TYPE を見てみます.

#define HECOMI_GETTYPE_CONTAINER_INNER_TYPE(z, n, max)\
	Type<BOOST_PP_CAT(T, n)>::Get()\
	BOOST_PP_IF(BOOST_PP_EQUAL(n, BOOST_PP_SUB(max, 1)), + , + ", " +)

第1引数は使わないので無視です.第2引数に現在の繰り返し回数,第3引数に渡した文字列が来ます.すなわち,

// BOOST_PP_REPEAT(3, HECOMI_GETTYPE_CONTAINER_INNER_TYPE, 3) =
HECOMI_GETTYPE_CONTAINER_INNER_TYPE(z, 0, 3) HECOMI_GETTYPE_CONTAINER_INNER_TYPE(z, 1, 3) HECOMI_GETTYPE_CONTAINER_INNER_TYPE(z, 2, 3)

となるわけです.従って「T」と「n」をくっつけて T0 や T1 といったテンプレート引数を生成してあげれば std::map の時に見たような Get() が作れるわけですね.この文字列を結合してくれる役割を果たすマクロが BOOST_PP_CAT です.3行目ではテンプレート引数が最後の場合以外は間に「,」を入れたいので最後かどうかをチェックするために BOOST_PP_IF をしています.第1引数に BOOST_PP_EQUAL という値を比較するマクロを,第2引数に真の場合に展開される文字列,第3引数に偽の場合に展開される文字列を入れておきます.繰り返しは0スタートなので引き算してくれるマクロ BOOST_PP_SUB で 1 だけ減算しています.全体としては次のような流れで展開が行われます.

BOOST_PP_REPEAT(3, HECOMI_GETTYPE_CONTAINER_INNER_TYPE, 3)
//		↓
HECOMI_GETTYPE_CONTAINER_INNER_TYPE(z, 0, 3)
HECOMI_GETTYPE_CONTAINER_INNER_TYPE(z, 1, 3)
HECOMI_GETTYPE_CONTAINER_INNER_TYPE(z, 2, 3)
//		↓
Type<BOOST_PP_CAT(T, 0)>::Get()
BOOST_PP_IF(BOOST_PP_EQUAL(0, BOOST_PP_SUB(3, 1)), + , + ", " +)
Type<BOOST_PP_CAT(T, 1)>::Get()
BOOST_PP_IF(BOOST_PP_EQUAL(1, BOOST_PP_SUB(3, 1)), + , + ", " +)
Type<BOOST_PP_CAT(T, 2)>::Get()
BOOST_PP_IF(BOOST_PP_EQUAL(2, BOOST_PP_SUB(3, 1)), + , + ", " +)
//		↓
Type<T0>::Get() BOOST_PP_IF(BOOST_PP_EQUAL(0, 2), + , + ", " +)
Type<T1>::Get() BOOST_PP_IF(BOOST_PP_EQUAL(1, 2), + , + ", " +)
Type<T2>::Get() BOOST_PP_IF(BOOST_PP_EQUAL(2, 2), + , + ", " +)
//		↓
Type<T0>::Get() + ", " +
Type<T1>::Get() + ", " +
Type<T2>::Get() +

これで次のように定義をしてあげれば目出度く std::vector も std::map も動作します.もちろん boost::tuple のようなもっと多い引数を取るものも可能です.

HECOMI_GETTYPE_CONTAINER_TYPE(std::vector, 1)
HECOMI_GETTYPE_CONTAINER_TYPE(std::map, 2)
HECOMI_GETTYPE_CONTAINER_TYPE(boost::tuple, 3)
int main()
{
	std::vector<int> a;
	std::vector<float*> b;
	std::vector<std::vector<int> > c;
	std::map<int, std::string> d;
	boost::tuple<int, float, std::vector<std::string> > e;
	std::cout << hecomi::GetTypeName(a) << std::endl; // std::vector<int>
	std::cout << hecomi::GetTypeName(b) << std::endl; // std::vector<float*>
	std::cout << hecomi::GetTypeName(c) << std::endl; // std::vector<std::vector<int>>
	std::cout << hecomi::GetTypeName(d) << std::endl; // std::map<int, std::string>
	std::cout << hecomi::GetTypeName(e) << std::endl; // boost::tuple<int, float, std::vector<std::string>>
	return 0;
}

コード全文

では最後に当初の目的である「std::vector >」のように出力させるコードを,これまでのコード全文と併せて載せておきます.先ほどとの違いは std::vector の引数を2個としているだけですね.
実行結果: http://codepad.org/iX6Bu8ZZ

#include <iostream>
#include <string>
#include <vector>
#include <map>
#include <boost/preprocessor.hpp>

namespace hecomi {

template <typename T>
struct Type
{
	static std::string Get() {
		return "undefined";
	}
};

#define HECOMI_GETTYPE_BASE_TYPE(type)\
	template <>\
	struct Type<type>\
	{\
		static std::string Get() {\
		return #type;\
		}\
	};\
	template <>\
	struct Type<type*>\
	{\
		static std::string Get() {\
		return #type"*";\
		}\
	};

HECOMI_GETTYPE_BASE_TYPE(int)
HECOMI_GETTYPE_BASE_TYPE(float)
HECOMI_GETTYPE_BASE_TYPE(std::string)


#define HECOMI_GETTYPE_CONTAINER_INNER_TYPE(z, n, max)\
	Type<BOOST_PP_CAT(T, n)>::Get()\
	BOOST_PP_IF(BOOST_PP_EQUAL(n, BOOST_PP_SUB(max, 1)), , + ", " +)

#define HECOMI_GETTYPE_CONTAINER_TYPE(type, n)\
	template <BOOST_PP_ENUM_PARAMS(n, typename T)/* typename T##n, ... */>\
	struct Type<type<BOOST_PP_ENUM_PARAMS(n, T)/* T##n, ... */> >\
	{\
		static std::string Get() {\
			return #type"<" + BOOST_PP_REPEAT(n, HECOMI_GETTYPE_CONTAINER_INNER_TYPE, n) + ">";\
		}\
	};

HECOMI_GETTYPE_CONTAINER_TYPE(std::vector, 2)
HECOMI_GETTYPE_CONTAINER_TYPE(std::allocator, 1)

template <typename T>
std::string GetTypeName(const T& x)
{
	return Type<T>::Get();
}

}

int main()
{
	std::vector<int> v;
	std::cout << hecomi::GetTypeName(v) << std::endl;
	return 0;
}