[C++]enumと文字列の相互変換を行う方法はある。

実は非常に美しく実現する方法がある、という話。以下は特にMSVCの話で、GCC他ではまた少し仕様が異なっているのだが、あるライブラリを使えばMSVC、Clang、GCCXcodeいずれでも環境の差が吸収され簡単にできるようになる。長々とした説明を読みたくない人は下のmagic_enumという項までスクロールすべし。

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

enum Month { Jan = 1, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec, };
int i = (int)Jan;//i == 1
int j = (int)Oct;//j == 10

ただ例えば標準出力などにその名前の方を出力したいと思っても、ちょっと手間がかかる。enumは本質的にはただの整数値なので、例えばprintfの%sに直接enumを渡すことは出来ない。もし文字列に変換したいのなら、通常はswitch文などを使って値ごとに処理を分岐しなければならない。と一般には思われている。

Month e = Feb;
//printf("%s\n", e); これは不可能。
switch (e)
{
case Jan: printf("Jan"); break;
case Feb: printf("Feb"); break;
︙
}

これはCやC++の初心者でも理解できる単純な方法という意味では優れている。しかしswitchなどで分岐する場合は記述コストが大きくなるし、列挙型に新たなメンバを追加した際は修正しなければならない。またenumと文字列を相互に変換することはできない。

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

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

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

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

enumのメンバ名の文字列を得るためのテクニック

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

int func(int x)
{
    std::cout << __FUNCSIG__ << std::endl;//int __cdecl func(int)
    return x;
}

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

enum Month { Jan = 1, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec, };

template <class M, M m>
void func()
{
    std::cout << __FUNCSIG__ << std::endl;//void __cdecl func<enum Month,Jan>(void)
}

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

#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];
};

template <class M, M m>
constexpr auto EnumName()
{
    constexpr char name[] = { __FUNCSIG__ };
    constexpr size_t N = sizeof(name) - 39;
    return StaticString<N>(name + 31);
}
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>;
    constexpr size_t Size = Max;
    static const auto& names = EnumNames<Enum, std::make_integer_sequence<UType, Size>>::msNames;
    return names[(UType)e];
}

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

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

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

しかし列挙型は場合によっては、0以外から始まる整数、非連続な整数などをメンバとして持つ場合もある。上で出てきたMonthなんてまさに1から始まる12までの整数なので、超適当に実装したGetEnumNameでは対応できない。しかしC++に列挙型の値の一覧を取得する方法が用意されているわけでもないので、これを実装するのはいろいろな小技が必要になり、非常に手間だ。またenum classを用いた場合も若干表示が狂う。しかも上の実装はMSVC用なので、他の環境ではこれまた正常に動作しない。何とかならないのだろうか?

magic_enum

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

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

enum Month { Jan = 1, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec, };

int main()
{
    std::cout << magic_enum::enum_name(Jan) << std::endl;//Jan
    std::cout << magic_enum::enum_cast<Month>("Feb").value() << std::endl;//2
    return 0;
}

こんなにも簡単に相互変換できてしまう。こんなもん絶賛せずにいられるか。
惜しむらくは、C++17以上でないと利用できないことか。おかげで私は導入したくても出来なかったりする。誰かC++11か14で使えるように改造してくれ。私は面倒だからやりたくない。

[C++]Gnuplotライブラリを各軸の文字列表示に対応させた。

 タイトルのとおりである。GitHubで公開しているGnuplotライブラリADAPT-GPM2を、以下の図のように、軸を数値ではなく文字列にできるようにした。

f:id:thayakawa:20200626000858p:plain

#include <ADAPT/GPM2/GPMCanvas.h>

using namespace adapt::gpm2;

int main()
{
    std::vector<std::string> x;
    std::vector<double> y;

    x.push_back("label-one");   y.push_back(1);
    x.push_back("label-two");   y.push_back(2);
    x.push_back("label-three"); y.push_back(3);
    x.push_back("label-four");  y.push_back(4);
    x.push_back("label-five");  y.push_back(5);
    x.push_back("label-six");   y.push_back(6);
    x.push_back("label-seven");   y.push_back(7);
    x.push_back("label-eight"); y.push_back(8);
    x.push_back("label-nine");  y.push_back(9);
    x.push_back("label-ten");  y.push_back(10);

    GPMCanvas2D g("string_label.png");
    g.ShowCommands(true);
    g.SetXRange(-1, 10);
    g.SetYRange(0, 11);
    g.SetXticsRotate(-45);
    g.PlotPoints(x, y, plot::style = Style::boxes, plot::title = "notitle");

    return 0;
}

 あとは少々バグフィックスされている。ついでにライセンスをMITに変更した。

 variantはマジで便利である。共用体?何それ食えんの?と言いたくなるくらい便利である。共用体はC++11である程度強化されたとは言え、それを定義するのは結構面倒くさい。variantは全部勝手に処理してくれるのでとても使い勝手が良い。今回の拡張も、ちょっとvariantにstd::vector<std::string>を追加して、以前紹介したラムダ式オーバーロードテクニックを使うだけで対応できた。C++17は本当に良いアップデートだった。……使ってないけど。私のライブラリはC++14縛りがあるので、使いたくても使えないのだ。代わりに必要になったら全部自作している。

[C++]std::decayの役割。

std::decayは放射性のstd::atomicを非放射性へと変換するための機能である(嘘)1

std::decayはよくわからない機能である。標準ライブラリの実装などを見ていると結構な頻度で見かけたりするのだが、一体何を目的に使用されているのかはすぐには理解できない。ただ関数型と配列型について考えてみるとなんとなくその用途が理解できるような気がするので、自分なりの理解をちょっと書き留めておく。
本稿の内容はC++規格書を大して読んだことのない人間の勝手な推測である。誤りがある可能性を十分考慮してもらいたい。また誤りに気がついた場合はコメントにて指摘してもらえると嬉しい。

std::decayの機能

std::decayは以下の機能をまとめたものである。

  • const、volatile、参照を取り除く。
  • 関数型を関数ポインタ型に変換する。
  • 配列型をポインタに変換する。

そもそも何故これらの変換が一纏めにされているのだろう? その理由は、たぶん次のことで説明できる。

関数テンプレートの型推論

次のようなコードがあったとき、Typeがどのように推定されるのかを考えてみる。

template <class Type>
void PrintType(Type v)
{
    printf("Type = ", typeid(v).name());
}
int func(int x) { return x; }

int main()
{
    int a = 0;
    const int& b = a;
    int c[4];
    print(a);//Type = int
    print(b);//Type = int
    print(c);//Type = int * __ptr64
    print(func);//Type = int (__cdecl*)(int)
    return 0;
}

上から順に、Typeはint、int、int*、int(*)(int)である。分かるだろうか、これらのTypeはstd::decayを通した型と全く同じなのだ。

using Type1 = typename std::decay<int>::type;//int
using Type2 = typename std::decay<const int&>::type;//int
using Type3 = typename std::decay<int(&)[4]>::type;//int*
using Type4 = typename std::decay<int(int)>::type;//int(*)(int)

std::decayの本質は、上のようなテンプレート型推論と同等な動作をさせることである。ってエラい人が言ってた。

特に面白いのは配列型と関数型の取り扱いだ。次のようなクラスを考えてみよう。

まず配列型について。配列型という型そのものは存在するが、一般に配列は代入演算子によるコピー、ムーブはできない。初心者以外にとっては常識だろう。古めかしいC言語のクソ仕様である。

int a[4] = { 1, 2, 3, 4 };
int b[4] = a;//error

同様に、もしテンプレート型推論で配列型がポインタへと変換されなかったら、上の"print(c)"という関数呼び出しは値渡しに失敗してエラーになるだろう。尤もC言語仕様により配列型引数は強制的にポインタとして扱われるので、型推論の仕組みに関わらず動作はするかもしれないが。

では関数型はどうか。"関数ポインタ型"はよく知られているが、C++には"関数型"も存在する。例えば上の例では、"func"は"int(int)"という関数型で、"&func"は"int(*)(int)"という関数ポインタ型だ。
ただし、関数ポインタ型の変数は存在するが、関数型の変数は存在しないと思われる2

using Func = int(int);//型としての"関数型"は存在する。
Func f;//関数fの宣言と解釈されるらしい。
int f(int x) { return x * 2; }//こんな感じに宣言に対応する定義を書くこともできるようだ。
using FuncPtr = int(*)(int);//"関数ポインタ型"ももちろん存在する。
int(*f)(int) = &func;//OK
int f(int) = func;//error fは単なる関数と認識されるので、funcを代入することはできない。
int(&f)(int) = func;//OK 関数型への参照は許される。

関数型とはつまり関数そのもののことなので、変数にはできないということだろう。 ということは、もしテンプレート型推論で関数型から関数ポインタ型への変換が行われない場合、配列のときと同様にprint(func)はコンパイルエラーとなるだろう。まあprint(&func)として関数ポインタ型で渡せばいい話ではあるのだが、参照渡しが一般化しているC++でいちいち&記号を要求されるのは変な話だ。

さて、総括すると、テンプレート型推論の規則はconstでもvolatileでもreferenceでもない、代入可能な変数型に推定する目的で定義されているのだと考えられる。

std::decayはコピー可能な変数型への変換機能

std::decayもこれと同じ役割を持たせたいのではなかろうか。つまり、テンプレート型推論の働かない状況下で、型推論と同様に格納可能な変数型を判定してくれるような機能がほしいのだ。次のような例を考えてみる。

template <class Type>
struct Test
{
    Test(Type v) : v(v) {}
    Type v;
};
int func(int x) { return x; }
int main()
{
    int a = 1;
    int b[4] = { 1, 2, 3, 4 };
    Test<int> t1(a);//OK
    Test<int[4]> t2(b);//error
    Test<int(int)> t3(func);//error
}

このクラスは、通常のプリミティブ型やクラスを与えることはできるが、関数型や配列を与えてしまうとコンパイルエラーになる。上で見たように、関数型の変数などというものは存在しないし、配列の場合はコンストラクタの初期化で弾かれてしまう。
ただし、std::decayを通せば別である。

template <class Type>
struct Test2
{
    using DecayedType = typename std::decay<Type>::type;
    Test2(DecayedType v) : v(v) {}
    DecayedType v;
};
int func(int x) { return x; }
int main()
{
    int a = 1;
    int b[4] = { 1, 2, 3, 4 };
    Test2<int> t1(a);//OK
    Test2<int[4]> t2(b);//OK
    Test2<int(int)> t3(func);//OK
}

std::decayを通せば普通の値でも配列でも関数でも全てメンバ変数として格納可能な形になるのだ。もちろんこれはメンバ変数として使うときばかりではなくて、引数を受け取るときでもローカル変数を宣言するときでも同様だ。

配列型を引数に渡す機会なんて早々ない、と思われるかもしれないが、案外そうでもない。例えば誰しもよく使うものとしては文字列リテラルがある。

template <class Type>
Test<Type> MakeTest(Type& v) { return Test<Type>(v); }
template <class Type>
Test2<Type> MakeTest2(Type& v) { return Test2<Type>(v); }
int main()
{
    auto t = MakeTest("abc");//error
    auto t2 = MakeTest2("abc");//OK
}

文字列リテラルはchar型配列なので、Test型を使うMakeTest関数ではエラーになる。しかし、std::decayを内蔵するTest2型であれば動作する。つい先日もPoco::Anyに文字列リテラルを渡してコンパイルエラーになるという問題に遭遇したばかりだ。このような機会は案外多い。

以上のように、std::decayは値渡し可能な変数型への変換が目的であり、通常の変数、関数、配列などの型を問わず、何らかの変数として実体化する際にエラーを起こさない目的で使われているのだ、と私は解釈した。


  1. std::decayについて調べているうちに某所で見つけたジョーク。ちょっと面白かった。

  2. 本当に存在しないのかどうかは規格書を読んでいない私にはわからないので注意されたい。