文字列と相互変換できる列挙型(enum)

stringとenumを双方向で変換できてかつPODなのが欲しくなったので書きました。内部の変換テーブルにはboost::bimapを使っています。

特徴としては

  • 双方向に変換できる
  • 実行時
  • POD型
  • boost::bimapなので速い(後述)

などです。

使用例

ENUM_UTILの実装は(長いので)記事の最後にあります。

#include <iostream>
#include "EnumUtil.h"
using namespace std;

//文字列変換できるenum
ENUM_UTIL(Number,
	ZERO,
	ONE,
	THREE = 3,
);

//クラス内にも定義可能
class MyClass {
	ENUM_UTIL(Fruit, APPLE, BANANA, ORANGE);
};

int main() {
	//文字列との相互変換
	Number zero = Number("ZERO");
	cout << to_string(zero) << endl;
	cout << to_string(Number::ONE) << " " << to_string(Number(Number::ONE)) << endl;

	//入出力
	Number n;
	cin >> n;
	cout << n << endl;

	//intとの変換
	int one = Number::ONE;
	Number three = static_cast<Number>(3);

	//比較
	assert(Number::ZERO == 0);
	assert(Number::ZERO < Number::ONE and Number::ONE < Number::THREE);

	//switch文
	switch (zero) {
	case Number::ZERO:
		break;
	default:
		assert(false);
	}
}

実行結果

ZERO
1 ONE
THREE	//input
THREE

速度

(主にbimapの)速度が不明だったので、要素が3つのenumを定義して列挙型→文字列→列挙型の変換を10億回繰り返してみました。

また、比較対象として

  • 列挙型→文字列はswitch、文字列→列挙型はifで分岐して変換
  • unordered_mapを2つ使って変換

も同時に計測しました。環境はgcc 5.1.0でオプションは-O3 -march=nativeです。

結果
方法 実行時間(ms)
ENUM_UTIL(今回・boost.bimap) 25937
switch & if 24045
unordered_map * 2 33817

結果自体は switch & if が一番速いですが、アルゴリズム的にはenumの要素数が増えればハッシュを使うbimapやunordered_mapの方が速くなってくることが予想されます。bimap速いんですねー。

実装

EnumUtil.h

#ifndef ENUMUTIL_H_
#define ENUMUTIL_H_

#include <type_traits>
#include <string>
#include <boost/algorithm/string.hpp>
#include <boost/bimap/bimap.hpp>
#include <boost/bimap/unordered_set_of.hpp>

namespace detail {
	template <class T>
	void initRegistry(boost::bimaps::bimap<boost::bimaps::unordered_set_of<T>, boost::bimaps::unordered_set_of<std::string>>& registry, std::string args) {
		//,で分解する
		std::vector<std::string> lines;
		boost::algorithm::split(lines, args, boost::is_any_of(","));

		T value = 0;
		for (std::string& line : lines) {
			boost::algorithm::trim(line);
			if (line.size() == 0) {
				continue;
			}

			//=で分解を試みる
			std::vector<std::string> terms;
			boost::algorithm::split(terms, line, boost::is_any_of(" ="), boost::algorithm::token_compress_on);
			if (terms.size() >= 2) {
				//分解できれば値を更新
				value = atoi(terms[1].c_str());
			}

			//ペアを追加
			registry.insert(typename boost::bimaps::bimap<boost::bimaps::unordered_set_of<T>, boost::bimaps::unordered_set_of<std::string>>::value_type(value, terms[0]));
			++value;
		}
	}

	//SFINAE用の基底クラス
	struct EnumUtilBase {};

	//入出力対応
	template <class T, class = typename std::enable_if<std::is_base_of<EnumUtilBase, T>::value>::type>
	std::string to_string(T t) {
		return t.to_string();
	}
	template <class T, class = typename std::enable_if<std::is_base_of<EnumUtilBase, T>::value>::type>
	std::istream& operator>>(std::istream& is, T& rhs) {
		std::string buf;
		is >> buf;
		rhs = T(buf);
		return is;
	}
	template <class T, class = typename std::enable_if<std::is_base_of<EnumUtilBase, T>::value>::type>
	std::ostream& operator<<(std::ostream& os, T rhs) {
		os << rhs.to_string();
		return os;
	}
}

//文字列変換付きenum
#define ENUM_UTIL(Name, ...)\
class Name : detail::EnumUtilBase {\
public:\
	enum Enum {\
		__VA_ARGS__\
	};\
	Name() = default;\
	Name(Enum value) {\
		enumHolder.value = value;\
	}\
	explicit Name(std::underlying_type<Enum>::type value) {\
		enumHolder.value = static_cast<Enum>(value);\
	}\
	explicit Name(const std::string& str) {\
		enumHolder.value = static_cast<Enum>(getRegistry().right.at(str));\
	}\
	operator Enum() const {\
		return enumHolder.value;\
	}\
	std::string to_string() const {\
		return getRegistry().left.at(enumHolder.value);\
	}\
private:\
	typedef boost::bimaps::bimap<boost::bimaps::unordered_set_of<std::underlying_type<Enum>::type>, boost::bimaps::unordered_set_of<std::string>> registry_type;\
	struct RegistryHolder {\
		RegistryHolder() {\
			detail::initRegistry(registry, #__VA_ARGS__);\
		}\
		registry_type registry;\
	};\
	static registry_type& getRegistry() {\
		static RegistryHolder registryHolder;\
		return registryHolder.registry;\
	}\
	struct EnumHolder {\
		Enum value;\
	} enumHolder;\
};\
static_assert(std::is_pod<Name>(), "ENUM_UTIL should be POD");

#endif /* ENUMUTIL_H_ */