C++ で文字列変換と型安全なビット演算ができる enum を定義するマクロ

アバター
lriki
記事: 88
登録日時: 14年前

C++ で文字列変換と型安全なビット演算ができる enum を定義するマクロ

投稿記事 by lriki » 10年前

・・・が必要になったので作ってみました。
せっかくなので公開してみます。

とりあえずコードを。(このヘッダだけ include すれば使えるようになります。VS2013で確認済み)
EnumExtension.h
(12.67 KiB) ダウンロード数: 229 回
▼(2015/7/6)h2so5さんの指摘修正ver
EnumExtension2.h
(12.92 KiB) ダウンロード数: 231 回
さて、単純なenum値⇔文字列変換を行うマクロであれば、検索すればいくつかヒットします。
http://www.singularpoint.org/blog/c/c-e ... ing-macro/
http://qiita.com/nunu-e64/items/e29075bdd82fad8ec28c

が、どうにも機能不足を感じます。

私はライブラリ書きで、人様に公開するモノとしては最低限以下のような条件を揃えたいと思うところでした。

・文字列と enum 値を相互変換できること。
・型安全なビット演算ができること。
・できるだけオリジナルの enum に近い記述で定義できること。
・メンバに Doxygen コメントを振れること。
・他の enum 値で値を定義できること。


まず、今回紹介するマクロを使った enum 定義の基本形です。

CODE:

// 定義
EX_ENUM(Color)
{
	Red = 0,
	Green,
	Blue,
};
EX_ENUM_DECLARE(Color);

// 使う
Color c = Color::Red;
突っ込みどころはたくさんあると思いますが、



まずは、話を聞いてくれ。
  _、_     
( ,_ノ` )     
     ζ
    [ ̄]'E



■文字列と enum 値を相互変換したい

XML 等のマークアップを利用するときに enum値⇔文字列 の変換が必要となるケースは割と良くあったりします。
C++ は C# とかみたいに enum がリッチじゃないので、
変換テーブルを書いたりと涙ぐましい努力が必要になります。

さて、次のように EX_ENUM_REFLECTION を定義に差し込みます。

CODE:

EX_ENUM(Color)
{
	Red = 0,
	Green,
	Blue,
};
EX_ENUM_REFLECTION(Color, Red, Green, Blue);	// enum メンバを並べる
EX_ENUM_DECLARE(Color);
すると、変換用のメンバ関数が使えるようになります。

CODE:

// 使う
Color c = Color::Red;
std::string str = c.ToString();	// 文字列へ → "Red"
c = Color::Parse("Blue");		// enum 値へ → Blue
.NET 使ったことある人にはなじみ深いスタイルで変換できます。


■型安全なビット演算がしたい

まずは普通の enum の問題点。
例えば…

CODE:

enum Color
{
	Color_Red = 0x01,
	Color_Green = 0x02,
	Color_Blue = 0x04,
};

void test(Color c) { }

int main()
{
	test(Color_Red | Color_Green);	// error
}
これはコンパイルエラーになります。
ビット和の結果は int なので、test の引数型を int にしなければなりません。

しかしそれでは test(123) のように書かれてしまってもコンパイルエラーとして検出できない (型の安全性が低い) ですし、
インテリセンスに enum 型が出てこないので test() の引数に何を指定するべきかすぐにわかりません。(「定義へ移動」できないのが辛い…)
この問題は C++11 の enum class でも同様です。(こっちはビット和自体禁止しているようですが)

EX_ENUM_FLAGS マクロで enum を定義することで、enum class のような使用感でこの問題を解決できます。

CODE:

EX_ENUM_FLAGS(Color)
{
	Red = 0x01,
	Green = 0x02,
	Blue = 0x04,
};
EX_ENUM_FLAGS_DECLARE(Color);

void test(Color c) { }

int main()
{
	test(Color::Red | Color::Green);
}
カラクリは Qt の QFlags と同じです。
https://wiki.qt.io/QFlags_tutorial
冗長な部分をマクロで隠しているだけです。


■ビット演算でも文字列変換したい

定義に EX_ENUM_FLAGS_REFLECTION マクロを差し込みます。

CODE:

EX_ENUM_FLAGS(Color)
{
	Red = 0x01,
	Green = 0x02,
	Blue = 0x04,
};
EX_ENUM_FLAGS_REFLECTION(Color, Red, Green, Blue);	// enum メンバを並べる
EX_ENUM_FLAGS_DECLARE(Color);

// 使う
Color c = Color::Red | Color::Blue;
std::string str = c.ToString();		// 文字列へ → "Red|Blue"
c = Color::Parse("Red|Green");		// enum 値へ → Red|Green
デフォルトのセパレータは '|' ですが、ToString(",")、 Parse("Red,Green", ',') のようにすることで変更できます。
トークンの間には空白を許可しています。


■できるだけオリジナルの enum に近い記述で定義したい

先頭で紹介したリンク先では

CODE:

DECLARE_ENUM(color, colorName, RED, BLUE, GREEN);
のように、可変長引数を利用して1行で定義していました。

しかし、(個人的な主観に依るところも大きいですが)この記述は enum を、少なくともプログラマにとって
何か意味のあるブロックを定義しているとは一見わかりにくいです。

{ } は C++ プログラマにとって大きな意味を持つトークンです。
ですので、あえてマクロの外側に出しています。


■メンバに Doxygen コメントを振りたい

enum コメントの理想は以下のような感じです。

CODE:

/// 色を示す列挙型
enum Color
{
	Red,		///< 赤
	Green,		///< 緑
	Blue,		///< 青
	
	/// 1行では足りない場合は
	/// 複数行に分けでコメントを振れる。
	All = Red | Green | Blue,
};
先の例のように、関数形式マクロで囲まれていると現行の Doxygen は(Doxygenのプリプロセッサを使っても)解析してくれません。
つまり、以下のようなコメントはどう頑張っても拾ってくれません。

CODE:

/// 色を示す列挙型
DECLARE_ENUM(
	Color,
	Red,		///< 赤
	Green,		///< 緑
	Blue		///< 青
);
EX_ENUM は Doxygen の設定 (Preprosessor) で "EX_ENUM_DOXYGEN" シンボルを定義するだけで
理想通りコメントを振れます。

CODE:

/// 色を示す列挙型
EX_ENUM(Color)
{
	Red,		///< 赤
	Green,		///< 緑
	Blue,		///< 青
};
EX_ENUM_DECLARE(Color);

■他の enum 値で値を定義したい

enum としては当然の機能ですが、先の例のように関数マクロを使用しているとこれも通常は不可能です。
しかし、EX_ENUM は実際のメンバ定義をマクロの外側に出しているので問題ありません。

CODE:

enum class Values1
{
	A = 0,
	B,
	C,
};

CODE:

DECLARE_ENUM(
	Values2,
	D = Values1::C,	// NG
	E,
	F
);

CODE:

EX_ENUM(Values2)
{
	D = Values1::C,	// OK
	E,
	F,
};
EX_ENUM_DECLARE(Values2);

■最後に

テストプロジェクトも置いておきます。

[拡張子 zip は無効化されているため、表示できません]

原本はこちらです。
https://github.com/lriki/Lumino.Core/bl ... xtension.h
先頭に張り付けた EnumExtension.h は、こちらを STL 用に修正したものです。

今後問題があればこちらに修正が入りますので、
もしご利用いただける場合は必要に応じて差分をマージしてください。


・・・でも、こんなメタメタしい機能、boost にあってもよさそうだけど・・・私の探しが足りないだけでしょうか?
最後に編集したユーザー lriki on 2015年7月06日(月) 22:48 [ 編集 1 回目 ]

アバター
h2so5
副管理人
記事: 2212
登録日時: 14年前

Re: C++ で文字列変換と型安全なビット演算ができる enum を定義するマクロ

投稿記事 by h2so5 » 10年前

CODE:

EX_ENUM(Color)
{
    Red = 0x01,
    Green = 0x02,
    Blue = 0x04,
    White = Red | Green | Blue,
};

Color c = Color::Red | Color::Green | Color::Blue;
c.ToString(); // Whiteにしてくれるといいな〜(´・・)

あごみつ
記事: 17
登録日時: 10年前

Re: C++ で文字列変換と型安全なビット演算ができる enum を定義するマクロ

投稿記事 by あごみつ » 10年前

おお!すげえ!
まだあんまり読んでないけど、

CODE:

#define EX_ENUM(enumName) \
	class enumName : public EnumExtension::Enum \
	{ \
	private: \
		int	m_value; \
	public: \
		enum _##enumName; \
		typedef _##enumName enum_type; \
		enumName() : m_value(0) {} \
		enumName(enum_type v) : m_value(v) {} \
		inline operator int() const { return m_value; } \
		inline bool operator==(enumName right) const { return m_value == right.m_value; } \
		inline bool operator==(enum_type right) const { return m_value == right; } \
		inline bool operator!=(enumName right) const { return !operator==(right); } \
		inline bool operator!=(enum_type right) const { return !operator==(right); } \
		friend inline bool operator==(enumName::enum_type left, enumName right) throw(); \
		friend inline bool operator!=(enumName::enum_type left, enumName right) throw(); \
	}; \
	inline bool operator==(enumName::enum_type left, enumName right) throw() { return left == right.m_value; } \
	inline bool operator!=(enumName::enum_type left, enumName right) throw() { return left != right.m_value; } \
	enum enumName::_##enumName
こうしたら、

CODE:

EX_ENUM(Color)
{
	Red,
	Green,
	Blue,
};
みたいに、EX_ENUM_DECLAREなしに済むと思う
その代わり、EX_ENUM_REFLECTIONの中身もグローバルに出ちゃうから、まあ記述量との兼ね合いかな

それと、C++11以降なら
m_value の型は std::underlying_type::type にしとくと、
EX_ENUM(Color) : uint8_t みたいなのにも対応できる
最後に編集したユーザー あごみつ on 2015年7月05日(日) 17:22 [ 編集 4 回目 ]

あごみつ
記事: 17
登録日時: 10年前

Re: C++ で文字列変換と型安全なビット演算ができる enum を定義するマクロ

投稿記事 by あごみつ » 10年前

連投申し訳ない

どうも enum の前方宣言がそもそも C++11 だった上に、上のコードは規格準拠じゃないです
MSVC でコンパイル通ったのは、いつものキモい拡張だったようです

CODE:

#define _EX_ENUM_1(name) _EX_ENUM_2(name, int)
#define _EX_ENUM_2(name, underlying_t) \
class name \
{ \
public: \
	enum _##name : underlying_t; \
private: \
	underlying_t m_value; \
}; \
enum name::_##name : underlying_t

#define EX_ENUM(...) BOOST_PP_OVERLOAD(_EX_ENUM_, __VA_ARGS__)(__VA_ARGS__)
これで EX_ENUM(Color, uint8_t) のように型指定できる……はず
最後に編集したユーザー あごみつ on 2015年7月05日(日) 19:45 [ 編集 1 回目 ]

アバター
lriki
記事: 88
登録日時: 14年前

Re: C++ で文字列変換と型安全なビット演算ができる enum を定義するマクロ

投稿記事 by lriki » 10年前

おお!なんかこんな変態コードに興味持っていただいたようでありがとうございます!

>h2so5さん
修正して本文に EnumExtension2.h として張っておきました。
よろしければどぞ。

>あごみつ さん
私も C++11 や boost は勉強途中なのでまだまだ良い案が見つかるかもしれませんね。
張っていただいたコード、参考にさせていただきます。