Edited at

C++のenumの値を文字列にできるライブラリnameofがすごい


はじめに

2019/7/10追記


  • NAMEOF_TYPEの例が0.8.0以降のバージョンで動かない問題を修正しました (Thanks! @tyu_ru_cppさん)

C++だと「enumで定義した名前を文字列にしたい」というときに困ることがたまによくあります。他の言語だとToStringメソッドを使って文字列にできたりするのですが、C++だとそうはいきません。

Qtを使うと独自の黒魔術によって比較的楽に取得することができるのですけども、Qtを使わないピュアなC++な環境だとどーすんだろっていう感じです。

そこで、Siv3dの作者さんである@Reputelessさんが、まさにそのenumから文字列の変換ができるライブラリについて呟いておられまして、その機能と実装方法に感銘を受けたので、ここで紹介したいと思います。


[nameof C++]


Neargye/nameof: Nameof operator for modern C++, name of variable, name of type, name of function, name of macro, name of enum.

https://github.com/Neargye/nameof


このライブラリが、enumから文字列にできるライブラリです。

enumから文字列にする以外にもいろいろ機能があるので、紹介していきます(ほぼほぼ公式readme)。


変数名を文字列にする

std::cout << NAMEOF(somevar) << std::endl;

std::cout << NAMEOF(person.address.zip_code) << std::endl;

変数名.PNG


関数名を文字列にする

std::cout << NAMEOF(some_method<int,float>()) << std::endl;

std::cout << NAMEOF(somevar.boo<int>()) << std::endl;

関数名.PNG


enumの名前を文字列にする

auto c = Color::RED;

std::cout << NAMEOF_ENUM(c) << std::endl;

enum.PNG


変数の型名を文字列にする


nameofバージョン0.8.0以降

std::cout << NAMEOF_VAR_TYPE(somevar) << std::endl;

std::cout << NAMEOF_VAR_TYPE(person) << std::endl;
std::cout << NAMEOF_VAR_TYPE(c) << std::endl;

typename080.PNG


nameofバージョン0.7.0以前

std::cout << NAMEOF_TYPE(somevar) << std::endl;

std::cout << NAMEOF_TYPE(person) << std::endl;
std::cout << NAMEOF_TYPE(c) << std::endl;

typename070.PNG


マクロ名を文字列にする

std::cout << NAMEOF(__LINE__) << std::endl;

macro.PNG

すごい。お試しソースコードの全文は こちら


enum名の取得はどうやっているのか

この記事の本題はこれです。

auto c = Color::RED;

std::cout << NAMEOF_ENUM(c) << std::endl; // REDと表示される

一度変数に入れたenumの値が、文字列として表示されています。

[nameof C++]では一体どうやって実装しているのでしょうか。


実装方法

MSVCやClangには、関数のシグネチャを取得できる事前定義済みのマクロがあります。

このマクロは、テンプレート引数の値まで取得できるので、それを利用します。

※ここで利用する環境はMSVCの15.9.10です。(gccやclangでは__PRETTY_FUNCTION__マクロですね)

基本的な原理原則は、以下のテンプレート関数です。

template <typename E, E V>

void nameof_impl() {
std::cout << __FUNCSIG__ << std::endl;
}

この関数にenumな型や値を渡すと、、、?

#include <iostream>


enum Hoge {
piyo,
fuga,
hogepiyo
};

template <typename E, E V>
void nameof_impl() {
std::cout << __FUNCSIG__ << std::endl;
}

int main() {
nameof_impl<Hoge, piyo>(); // void __cdecl nameof_impl<enum Hoge,piyo>(void)
}

この例の場合、コメントで示したようにvoid __cdecl nameof_impl<enum Hoge,piyo>(void)と表示されます。

このままだとコンパイル時に解決されてしまってあまり意味が無いので、1枚泥臭い関数をかませます。

#include <iostream>


enum Hoge {
piyo,
fuga,
hogepiyo
};

template <typename E, E V>
void nameof_impl() {
std::cout << __FUNCSIG__ << std::endl;
}

template <typename E>
void nameof(E e) {
switch (e) {
case 0:
nameof_impl < E, E{ static_cast<E>(0) } > ();
break;
case 1:
nameof_impl < E, E{ static_cast<E>(1) } > ();
break;
case 2:
nameof_impl < E, E{ static_cast<E>(2) } > ();
break;
case 3:
nameof_impl < E, E{ static_cast<E>(3) } > ();
break;
default:
std::cout << "oops..." << std::endl;
break;
}
}

int main() {
const auto h = static_cast<Hoge>(0);
nameof(h); // void __cdecl nameof_impl<enum Hoge,piyo>(void)
}

これにより、一つの引数からその引数の文字列を表示することができました。

コンパイル時と実行時を繋ぐのは難しいですね。

さて、変数に入っている値からvoid __cdecl nameof_impl<enum Hoge,piyo>(void)という文字列が取り出せました。

どうにかこの文字列からpiyoという文字列を切り出すことができれば、enumの文字列をとれることになります。

文字列リテラルをsizeofするとその文字列の長さが取れること、関数のシグネチャはある程度固定であることを利用すると、以下のように筋肉で切り出すことが出来ます。

#include <iostream>

#include <string>

enum Hoge {
piyo,
fuga,
hogepiyo
};

template <typename E>
size_t get_type_length() {
constexpr auto str = __FUNCSIG__;
constexpr auto str_len = sizeof(__FUNCSIG__);
constexpr auto prefix_len = sizeof("unsigned int __cdecl get_type_length<") - 1;
constexpr auto suffix_len = sizeof(">(void)");
return std::string{ str + prefix_len,str + str_len - suffix_len }.length();
}

template <typename E, E V>
void nameof_impl() {
constexpr auto str = __FUNCSIG__;
constexpr auto prefix_len = sizeof("void __cdecl nameof_impl<") - 1;
constexpr auto suffix_len = sizeof(">(void)");
constexpr auto size = sizeof(__FUNCSIG__);
std::cout << std::string{ str + prefix_len + get_type_length<E>() + 1,str + size - suffix_len };
}

template <typename E>
void nameof(E e) {
switch(e) {
case 0:
nameof_impl < E, E{ static_cast<E>(0) } > ();
break;
case 1:
nameof_impl < E, E{ static_cast<E>(1) } > ();
break;
case 2:
nameof_impl < E, E{ static_cast<E>(2) } > ();
break;
case 3:
nameof_impl < E, E{ static_cast<E>(3) } > ();
break;
default:
std::cout << "oops..." << std::endl;
break;
}
}

int main() {
const auto h = static_cast<Hoge>(2);
nameof(h);
}

基本的な方法はこんな感じです。

ただしこの実装は、簡略化のため整数0から3の範囲にあるenumしか名前が表示されません。nameof of C++の実装では、メタプログラミングで-256から+255の範囲を探索しているようです。


まとめ

というわけで、いろいろな名前を表示するライブラリの紹介と、enumの表示どうやってるのの紹介でした。

__FUNCSIG__マクロで取れる文字列を利用するというのは今まで思いつかなかったので、目から鱗です。

wandboxで試したところ、gccだと4.6.4くらいから、clangだと3.1からコンパイルはできるようです。ただし、__PRETTY_FUNCTION__マクロが意図した文字列に置き換わってくれるかというと、、、微妙です。

扱いが難しい、、、