はじめに
2019/7/10追記
- NAMEOF_TYPEの例が0.8.0以降のバージョンで動かない問題を修正しました (Thanks! @tyu_ru_cppさん)
C++だと「enumで定義した名前を文字列にしたい」というときに困ることがたまによくあります。他の言語だとToStringメソッドを使って文字列にできたりするのですが、C++だとそうはいきません。
Qtを使うと独自の黒魔術によって比較的楽に取得することができるのですけども、Qtを使わないピュアなC++な環境だとどーすんだろっていう感じです。
そこで、Siv3dの作者さんである@Reputelessさんが、まさにそのenumから文字列の変換ができるライブラリについて呟いておられまして、その機能と実装方法に感銘を受けたので、ここで紹介したいと思います。
C++ で enum の値を追加の記述無しで文字列にできるライブラリだ (MSVC, Clang で動作) https://t.co/fuxAMcVKFJ pic.twitter.com/HwxJWnmcTd
— Ryo Suzuki (@Reputeless) March 22, 2019
[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;
関数名を文字列にする
std::cout << NAMEOF(some_method<int,float>()) << std::endl;
std::cout << NAMEOF(somevar.boo<int>()) << std::endl;
enumの名前を文字列にする
auto c = Color::RED;
std::cout << NAMEOF_ENUM(c) << std::endl;
変数の型名を文字列にする
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;
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;
マクロ名を文字列にする
std::cout << NAMEOF(__LINE__) << std::endl;
すごい。お試しソースコードの全文は こちら
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__マクロが意図した文字列に置き換わってくれるかというと、、、微妙です。
扱いが難しい、、、