[C++]enumを文字列に変換する汎用的な方法。

2021年9月28日追記。どういうわけかアクセス数が増えているので、訪問者を混乱させないよういい加減だったサンプルを作り直した。

enumは単なる整数値に名前を付与する手段の一つだ。パラメータの意味がわかりやすくなる上、処理コストの大きな文字列を使わなくても良い便利な機能である。最近ではenum classの形で使われることが推奨されている。

enum DoW { Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, };
DoW dow = Sunday;
int i = (int)dow;//i == 0

ただ例えば標準出力などにその名前の方を出力したいと思っても、ちょっと手間がかかる。enumは本質的にはただの整数値なので、例えばprintfの%sに直接enumを渡すことは出来ない。ネット上に溢れかえっている方法は残念ながらコードが簡便であるがenumのメンバを修正したりするたびにメンテナンスコストが発生するので厄介だ。

が、実は非常に美しく実現する方法がある。もし自力で実装するのが面倒なら、magic_enumというライブラリを使えばMSVC、Clang、GCCXcodeいずれでも環境の差が吸収され簡単にできるようになる。長々とした説明を読みたくない人は下のmagic_enumという項までスクロールすべし。

愚直な方法

もし文字列に変換したいのなら、通常はswitch文などを使って値ごとに処理を分岐しなければならない。と一般には思われている。

enum DoW { Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, };
DoW e = Tuesday;
//printf("%s\n", e); これは不可能。
switch (e)
{
case Sunday: printf("Sunday\n"); break;
case Monday: printf("Monday\n"); break;
case Tuesday: printf("Tuesday\n"); break;
...
}

これはCやC++の初心者でも理解できる単純な方法という意味では優れている。しかし記述コストが大きくなるし、列挙型に新たなメンバを追加した際は修正しなければならない。

std::unordered_mapboost::bimapsを使って対応付けることで変換することも出来るが、メンテナンスコストは大きいままだ。

あるいはマクロを使う手もしばしば提示されるが、これは良くない。マクロはコンパイルの直前に展開されるため、変数の中身は文字列化できないのだ。

#define VarName(name) #name
printf("%s\n", VarName(Tuesday));//これは正しく"Tuesday"と出力されるが、
DoW e = Tuesday;
printf("%s\n", VarName(e));//これでは"Tuesday"ではなく"e"と出力される。

だが実は、他にも方法がある

少しスマートな方法

自前で分岐させる他にも、enumの変数名を取得する方法は存在しないわけではないのである。鍵となるのは__FUNCSIG__(Clang、GCC等では__PRETTY_FUNCTION__)という定義済みマクロだ。これは関数の情報を返してくれるもので、例えば次のようにfuncという関数内でこのマクロの中身を見てみると、

int func(int x)
{
    //MSVCの場合
    std::cout << __FUNCSIG__ << std::endl;//int __cdecl func(int)
    //GCC、Clangの場合
    std::cout << __PRETTY_FUNCTION__ << std::endl;//int func(int)
    return x;
}

このように、マクロの外側の関数についての情報を教えてくれる。これを利用する。次の関数を考えてみよう。

enum DoW { Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, };

template <class Enum, Enum E>
void func()
{
    //Enum = Dow, E = Sundayが与えられているとする。
    //MSVCの場合
    std::cout << __FUNCSIG__ << std::endl;//void __cdecl func<enum DoW,Sunday>(void)
    //GCC、Clangの場合
    std::cout << __PRETTY_FUNCTION__ << std::endl;//void func() [Enum = DoW, E = Sunday]  GCCとClangで微妙に異なる。
}

面白いことに、テンプレートパラメータにenumのメンバを渡すと、__FUNCSIG__の関数名にそのメンバの文字列が含まれるようになるのだ。これはtypeidなどには出来ない芸当である。あとはこの情報から前半と後半に付随する余計な文字を削除すれば、enumから文字列への変換が可能になる。え、これじゃ実行時変数からは文字列に変換できないじゃんって?そこは配列の出番である。予め"enumの各メンバに対応する文字列を格納した配列"を生成しておき、実行時にそこから適宜取り出せばよい。

この仕組をとてもいい加減に作ってみると以下のような感じになる。 とりあえずMSVC2019、Clang13.0.0、GCC12.0.0で動作を確認した。C++14以上で動くはずである。

#include <iostream>
#include <string>
#include <utility>
#include <array>

template <size_t N>
struct StaticString
{
    StaticString(const char* c)
        : StaticString(c, std::make_index_sequence<N>())
    {}

    std::string str() const { return std::string(mStr); }

private:
    template <size_t ...Indices>
    StaticString(const char* c, std::index_sequence<Indices...>)
        : mStr{ c[Indices]..., '\0' }
    {}

    char mStr[N + 1];
};

constexpr size_t FindChar(const char* str, char f)
{
    size_t res = 0;
    while (*str != f && *str != '\0') ++res, ++str;
    return res;
}

template <class M, M m>
constexpr auto EnumName()
{
#ifdef _MSC_VER
    constexpr char name[] = { __FUNCSIG__ };
    constexpr size_t s = FindChar(name, ',') + 1;
    constexpr size_t e = FindChar(name, '>');
#else
    constexpr const char* name_ = __PRETTY_FUNCTION__;
    constexpr const char* name = name_ + FindChar(name_, '=') + 1;
    constexpr size_t s = FindChar(name, '=') + 2;
    constexpr size_t e = FindChar(name, ']');
#endif
    constexpr size_t N = e - s;
    return StaticString<N>(name + s);
}
template <class Enum, class Indices>
struct EnumNames;
template <class Enum, std::underlying_type_t<Enum> ...Indices>
struct EnumNames<Enum, std::integer_sequence<std::underlying_type_t<Enum>, Indices...>>
{
    static const std::array<std::string, sizeof...(Indices)> msNames;
};
template <class Enum, std::underlying_type_t<Enum> ...Indices>
const std::array<std::string, sizeof...(Indices)>
EnumNames<Enum, std::integer_sequence<std::underlying_type_t<Enum>, Indices...>>::msNames
= { EnumName<Enum, (Enum)(Indices)>().str()... };

template <class Enum, std::underlying_type_t<Enum> Max = 128>
std::string GetEnumName(Enum e)
{
    using UType = std::underlying_type_t<Enum>;
    static const auto& names = EnumNames<Enum, std::make_integer_sequence<UType, Max>>::msNames;
    return names[(UType)e];
}

enum DoW { Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, };
enum class Month { January, February, March, April, May, June, July, August, September, October, November, December, };

int main()
{
    std::cout << GetEnumName(Monday) << std::endl;//Monday
    std::cout << GetEnumName(Saturday) << std::endl;//Saturday
    std::cout << GetEnumName(Month::August) << std::endl;//Month::August
    return 0;
}

特に複雑なことはしていない。単に__FUNCSIG__または__PRETTY_FUNCTION__によって得られる文字列からenumのメンバ名の部分を切り取り、配列に収め、GetEnumNameで配列から該当する名前を取得しているだけである。

この方法の最大の利点は、あらゆるenumに対して使えることだ。新たなenumを定義したりメンバを追加したりしても上のコードを書き直す必要がない。

しかし問題もある。上の実装はenumの各値が0から127までの整数であることを前提としており、その範囲外の値を取る場合は対応できない。しかしC++に列挙型のメンバの一覧を取得する方法が用意されているわけでもないので、これを汎用的に実装するのはいろいろな小技が必要になり、非常に手間だ。

magic_enum

そこで登場するのがmagic_enumである。このあたりのややこしいメタプログラミングを全部実装してくれていて、非常に簡単にenumと文字列の相互変換を可能にしてくれる最高のライブラリだ。相互変換である。上ではenumから文字列への変換しか実装しなかったが、こちらは文字列からenumへの変換まで可能にしてくれる万能っぷり。その実装の美しさはもうコードを読み進めるごとに感動で涙してしまうほどである。シングルヘッダーなのでファイル一つコピペしてincludeするだけで使えるところも素晴らしい。

#include <iostream>
#include <magic_enum.hpp>

enum DoW { Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, };

int main()
{
    std::cout << magic_enum::enum_name(Sunday) << std::endl;//Sunday
    std::cout << magic_enum::enum_cast<DoW>("Wednesday").value() << std::endl;//3
    return 0;
}

こんなにも簡単に相互変換できてしまう。絶賛せずにいられるか。
C++17以上が必要、かつ依存ライブラリが増えてしまうが、それが問題にならないのなら導入するとよいだろう。それが不可能なら上のサンプルを頑張って改造しよう。