16
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

C++Advent Calendar 2020

Day 7

C++における型名の文字列取得法を思いつく限り書いてみる。

Last updated at Posted at 2020-12-06

注意

記事執筆時点でC++20と明示しているソースに関しては、標準ライブラリ言語機能共に各コンパイラベンダが実装途中であり、
それらを受けて本来使うべきC++20の機能などを使っていない不完全なコードの可能性があります。
それ以外のソースはC++17で書いております。
また各テスト環境は
gcc 10.2
clang 11.0.0
VS2019 16.8.2
です。

また、本記事の方法を使うこと、参考とすることによる不利益は一切負うことができませんので、ご自身の判断、責任のもとお使いください。
また、__面倒なのでenumは明記していない場合、扱っていません。__後日気力があれば追記します。

はじめに

唐突ですが、C++で型名が文字列で欲しいと思った瞬間、ありますよね。
主にはデバッグ用途、あるいはプラグインのあるソフトウェアを書かれる方など、ちょっと動的なことをするときに欲しいなと思うことがあるでしょう。
とはいえ、C#におけるnameof()のような構文はC++には存在しません。

方法1 そのまま書く

冗談みたいですが原始的な方法は以下のように、あきらめて文字列を直接書くことでしょう。
文字列を対象にリファクタリングできるエディタもあるようなので必ずしもそうとはいえませんが、
スマートではない上、複数人開発でこんなことをすれば面倒なバグを生みかねません。
個人的には絶対やりたくないです。

example.cpp
#include <iostream>

class Test {};

char const* Typename = "Test"; //これはやりたくない
int main()
{
    std::cout << Typename << std::endl;
    return 0;
}

##方法2 #(文字列化演算子)を使う
先ほど、nameofのような構文はC++には存在しないと書きました。
しかしながら、マクロには近いことが可能な存在があります。
そう、文字列化演算子です。
#がマクロの文脈に現れた際はパラメータをそのまま文字列化します。

example2.cpp
#include <iostream>
#define nameof(x) #x
class Test {};

int main()
{
    std::cout << nameof(Test) << std::endl;
    return 0;
}

リファクタリングも効くのでこれで良いかとも思いますが、 #は存在しない識別子だろうがなんだろうが問答無用で文字列にしてしまうという点があります。
つまり方法1と潜在的には似たようなリスクが存在します。
ソースコード上から消えたクラスの名前がある日突然現れる、といったちょっとしたホラーは避けたいですよね。
また展開の優先度からライブラリ内などでユーザーの型名を利用する場合はユーザにマクロを使用してもらう必要があります。
C++20からモジュールがヘッダーの代わりに使われますが、マクロはエクスポートできません。
まだまだ現役のマクロですが、時代を経るにつれレガシー化しかねないので、将来性を担保する必要がある場合も一考の余地があります。

この手法で利便性の高いライブラリは

bravikov/nameof
https://github.com/bravikov/nameof

です。
ragexを用いて得られた文字列の成型をしており、C#のnameofに近い使用感(XXX::AAAとあったらAAAだけを取得できる)があります。

方法3 typeid(実行時型情報)を使う

RTTIが許される環境ならこれで終わりです。
typeid(type)とした際にtype_infoが得られます。type_info::name()がお目当てのものです。
利点としては、型情報の生成を自前でやらずにコンパイラに任せることができる点(ただしコンパイラ依存のフォーマット)。
GCC,Clangはマングリングされた名前を返すようになっており、__cxxabi.h__のabi::__cxa_demangleでデマングルする必要があります。
MSVCはプレフィクスにclassやstructなどがつく内部表現っぽい出力でしたので、サンプルではstring_viewに詰めた後適切な長さでremove_preffixしています。

example3_gcc_clang.cpp
#include <iostream>
#include <cxxabi.h>

class Test {};

int main()
{
    int status;
    Test test;
    std::cout << std::endl;
    std::cout << abi::__cxa_demangle(typeid(test).name(),0,0,&status) << std::endl;
    //typeid(test).name() GCC、Clangでは"4Test"が戻り値。
    return 0;
}
example3_msvc.cpp
#include <iostream>
#include <string_view>

class Test {};

//classまたはstructのプレフィックスがつくのでclas"s"を見て先頭を削除
std::string_view skip(std::string_view name)
{
	if (name.size() < 7) return std::string_view();
	name.remove_prefix(name.at(4) == 's' ? 6 : 7);
	return name;
}

int main()
{
	Test test;
	std::cout << skip(typeid(test).name()) << std::endl;
	return 0;
}

実用する場合はコンパイラを判定して適切な形で出力する必要があります。

##方法4 __func__(事前定義識別子)を使う。
__func__ はC++11以降で利用可能なただ一つの事前定義識別子です。
関数内でのみ利用可能で、フォーマットはコンパイラ依存ながら関数名を文字列定数として取得できます。
ということで型名の入っている関数として、コンストラクタ内に以下のコードを挟むことで型名を文字列化して得ることができます。

example4.cpp
#include <iostream>

class Test {
    char const* typename_;
public:
    Test()
        :typename_{ __func__ }
    {}

    char const* GetTypename(){ return typename_; }
};


int main()
{
    Test test;
    std::cout << test.GetTypename() << std::endl;
    return 0;
}

##方法4B コンパイラ言語拡張を使う
GCC、Clang、MSVCに環境を絞っていいのであればもう少し良い出力を得られます。
またこの方法の最大の利点はtypeid、type_infoと違い__コンパイル時に文字列を確定できる__ということです。
実行時に型名の成型(デマングル)などはほぼないので、実質0コストで型名取得できます。
記事の都合上string_viewを使っていますが、C++17以前であれば泥臭くchar[]のテンプレートラッパーを作るような方法でも成立するはずです。

example4B_gcc_clang.cpp
#include <iostream>
#include <string_view>
union TestU {};
struct TestS {};
class TestC {};

template<class T>
constexpr std::string_view nameof()
{
	std::string_view name = __PRETTY_FUNCTION__; 
#if defined(__clang__)
    //Clang展開例:"std::string_view nameof() [T = TestU]"
	name.remove_prefix(31);
	name.remove_suffix(1);
#elif defined(__GNUC__)
    //GCC展開例:"constexpr std::string_view nameof() [with T = TestU; std::string_view = std::basic_string_view<char>]"
	name.remove_prefix(46);
	name.remove_suffix(50);
#endif
	return name;
}

int main()
{
	constexpr std::string_view nameU = nameof<TestU>();
	constexpr std::string_view nameS = nameof<TestS>();
	constexpr std::string_view nameC = nameof<TestC>();
	std::cout << nameU << std::endl;
	std::cout << nameS << std::endl;
	std::cout << nameC << std::endl;
	return 0;
}
example4B_msvc.cpp
#include <iostream>
#include <string_view>

union TestU {};
struct TestS {};
class TestC {};

template<class T>
constexpr std::string_view _cdecl nameof()
{
	//_cdeclを付けないとMSVCの場合アプリケーションの既定呼び出し規約オプションによって文字数がずれるため。
	std::string_view name = __FUNCSIG__;           //展開例:"class std::basic_string_view<char,struct std::char_traits<char> > __cdecl nameof<union TestU>(void)"
	name.remove_prefix(name[87] == ' ' ? 88 : 87); //87文字目が' 'ならstruct,それ以外はclass,union
	name.remove_suffix(7);
	return name;
}

int main()
{
	constexpr std::string_view nameU = nameof<TestU>();
	constexpr std::string_view nameS = nameof<TestS>();
	constexpr std::string_view nameC = nameof<TestC>();
	std::cout << nameU << std::endl;
	std::cout << nameS << std::endl;
	std::cout << nameC << std::endl;
	return 0;
}

この手法を内部で使っているライブラリは

Neargye/nameof
https://github.com/Neargye/nameof
です。
c++17以降であれば非常に有用な選択肢になるでしょう。
ユーザー側からはマクロ形式で利用する形になっていますが、内部ではコンパイラに応じて取得方法を切り替えたり、enum対応などかゆいところに手が届く作りです。

##方法5 source_locationクラスを使う
個人的な期待の星でした。C++20で追加されるこのsource_locationはリテラルであり、std::source_location::current()で現在の関数名やファイル名、行番号、列番号まで取得できる優れものです。
案の定、返される関数名はヌル終端であること以外は何も決まっていません=コンパイラ依存です。
MSVCはそもそも実装前、Clangは返された文字列にテンプレートの内容までは含まれていなかったので現状はGCCのみです。

example5_gcc.cpp
#include <iostream>
#include <experimental/source_location>
union TestU {};
struct TestS {};
class TestC {};

template<class T>
struct tag{};
template<class T>
constexpr std::string_view nameof(tag<T> __tag = tag<T>())
{
	std::string_view name = std::experimental::source_location::current().function_name(); 
        //GCC展開例:"nameof<TestU>"
	name.remove_prefix(7);
	name.remove_suffix(1);
	return name;
}

int main()
{
	constexpr std::string_view nameU = nameof<TestU>();
	constexpr std::string_view nameS = nameof<TestS>();
	constexpr std::string_view nameC = nameof<TestC>();
	std::cout << nameU << std::endl;
	std::cout << nameS << std::endl;
	std::cout << nameC << std::endl;
	return 0;
}

#結論
というわけでsource_locationクラスの紹介記事にしたかったのですが、思ったより貧弱難しいですね。
__早くstatic reflection1を下さい。__もうこんな環境依存のコードは書きたくないです。
本文中には書きませんでしたが、1ファイル1クラスの原則を守っているのであれば __FILE__ から疑似的に得る。
試してはいませんが、多段コンパイル方式にして、ソースコード、objファイル等から型名を収集した後生成、最終段でリンクするといったキワモノも想定されます。
もし、上記以外の方法をご存知の方がいらっしゃれば、特定のライブラリ、環境でのみ動くものでも是非教えていただければ幸いです。

  1. http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/p0194r3.html

16
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
16
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?