はじめに
静的リフレクションの話じゃないよ
先日、こちらの C++ Advent Calendar 2024 の18日目の記事を読ませていただきました。
C++に慣れた方であれば、
上記記事で紹介されているような、JSONなどの文字列形式から構造体への自動変換は
普通に実装しようとしても、できないことがわかると思います。
なぜなら、C++には現状リフレクションがないので、真っ当なやり方ではメンバ変数の個数や名前などが取得できないからです。
ん? 本当にできないのか?
よかろう、闇の力を授けよう。
できなそうに思えても、実際にできてしまってるので、どうやって実現してるのか気になる所ですので、さっそくglaze JSONの実装を潜ってみることにしました。
実は以前にも、SQLで取得したレコードを構造体に変換するライブラリで暗黒テクニックを見たことがあったので、それと同じ手法かなと思いつつ、
案の定、いつもの、実家のような安心感があるコードがありました。
解説
構造体のメンバの数の数え方
まずなんにでもキャストできる型を用意します。
// なんにでも暗黙キャストできる構造体
struct AnyCastable
{
// 実行時評価しないので実装は不要
template <class T>
consteval operator T() const;
};
そうしたらば、初期化に渡せる数を1つずつ増やしながら、初期化可能か再帰チェックすることでメンバ変数の数を調べます。
このような条件から、変換できる構造体は、集成体である必要があります。
条件を満たすかはstd::is_aggregate
でチェックできます。
集成体を満たす条件
- ユーザー定義されたコンストラクタ、
explicit
なコンストラクタ、継承コンストラクタを持たないprivate
/protected
な非静的メンバ変数を持たない- 仮想関数を持たない
- 仮想基底クラス、
private
/protected
基底クラスを持たない
template <class T, class... Args>
requires (std::is_aggregate_v<std::remove_cvref_t<T>>)
consteval size_t CountMembers()
{
// 集成体初期化に渡せる変数の数を1つずつ増やしていき
// 渡せなくなった時点での引数の数がメンバーの数
using V = std::remove_cvref_t<T>;
if constexpr (requires { V{ Args{}..., AnyCastable{} }; }) {
return CountMembers<V, Args..., AnyCastable>();
} else {
return sizeof...(Args);
}
}
struct MemberCount1
{
int a;
};
struct MemberCount2
{
int a;
float b;
};
struct MemberCount3
{
int a;
float b;
char c;
};
static_assert(CountMembers<MemberCount1>() == 1);
static_assert(CountMembers<MemberCount2>() == 2);
static_assert(CountMembers<MemberCount3>() == 3);
構造体をtuple
に変換する
これは力業です。
構造化束縛を使って、任意の数まで対応しましょう。
template <class T, size_t N = CountMembers<T>()>
constexpr decltype(auto) ToTuple(T&& t)
{
if constexpr (N == 0) {
return std::tuple{};
}
else if constexpr (N == 1) {
auto& [p0] = t;
return std::tie(p0);
}
else if constexpr (N == 2) {
auto& [p0, p1] = t;
return std::tie(p0, p1);
}
else if constexpr (N == 3) {
auto& [p0, p1, p2] = t;
return std::tie(p0, p1, p2);
}
// 以下サポートする数まで続く
}
// 構造体からタプルに変換
MemberCount3 m3{ 0, 1.0f, 'a' };
auto tuple = ToTuple(m3);
テンプレート関数のシグネイチャから情報を抜き取る
gcc,clangには__PRETTY_FUNCTION__
msvcに __FUNCSIG__
を使うことで関数しぐネイチャを取得できるのですが
この時、テンプレート引数に情報を入れておくことで、その名前を抜き取ることができます。
template <auto Ptr>
consteval auto GetSignature()
{
#if defined(__clang__) || defined(__GNUC__)
return __PRETTY_FUNCTION__;
#elif defined(_MSC_VER)
return __FUNCSIG__;
#endif
}
例として以下を実行します。
template <class T>
struct Ptr
{
const T* ptr;
};
// 構造体のN番目の変数のポインタを得る
template <size_t N, class T>
constexpr auto GetPtr(T&& t)
{
auto& p = get<N>(ToTuple(t));
return Ptr<std::remove_cvref_t<decltype(p)>>{&p};
}
template <size_t N, class T>
constexpr auto GetPtr()
{
using V = std::remove_cvref_t<T>;
return GetPtr<N, V>(V{});
}
template <auto N, class T>
consteval std::string_view GetMemberSignature()
{
return GetSignature<GetPtr<N, T>()>();
}
int main()
{
std::println(GetMemberSignature<0, MemberCount3>());
std::println(GetMemberSignature<1, MemberCount3>());
std::println(GetMemberSignature<2, MemberCount3>());
return 0;
}
出力する文字列は、コンパイラによって変わりますが、
msvcの場合以下のようになります。
auto __cdecl GetSignature<struct Ptr<int>{const int*:&->a}>(void)
auto __cdecl GetSignature<struct Ptr<float>{const float*:&->b}>(void)
auto __cdecl GetSignature<struct Ptr<char>{const char*:&->c}>(void)
↑ということで、メンバー変数の型情報とか、変数名が仕込まれていることがわかります。
メンバ変数名取得
struct REFLECTOR
{
int FIELD;
};
struct ReflectField
{
static constexpr auto name = GetMemberSignature<0, REFLECTOR>();
// >
static constexpr auto end = name.substr(name.find("FIELD") + sizeof("FIELD") - 1);
// }>(void)
static constexpr auto begin = name[name.find("FIELD") - 1];
};
// メンバ変数名取得
template <auto N, class T>
consteval std::string_view MemberNameof()
{
constexpr auto name = GetMemberSignature<N, T>();
// }>(void)を取り除く
constexpr auto begin = name.find(ReflectField::end);
constexpr auto tmp = name.substr(0, begin);
// 最後の>を探してそれ以前を取り除く
constexpr auto stripped = tmp.substr(tmp.find_last_of(ReflectField::begin) + 1);
return stripped;
};
static_assert(MemberNameof<0, MemberCount3>() == "a");
static_assert(MemberNameof<1, MemberCount3>() == "b");
static_assert(MemberNameof<2, MemberCount3>() == "c");
データの紐づけ
メンバ変数の数や名前がとれれば、あとは適当に回しておけば
文字列形式とやり取り可能です。
template<class T, class F, size_t N>
void apply(T&& t, F f)
{
auto& p = std::get<N>(ToTuple(t));
f(p, MemberNameof<N, T>());
}
template<class T, class F, size_t... Seq>
void apply(T&& t, F f, std::index_sequence<Seq...> seq)
{
(apply<T, F, Seq>(std::forward<T>(t), f), ...);
}
template<class T, class F>
void apply(T&& t, F f)
{
apply(std::forward<T>(t), f, std::make_index_sequence<CountMembers<T>()>());
}
int main()
{
std::unordered_map<std::string_view, std::any> map;
map["a"] = 100;
map["b"] = 200.0f;
map["c"] = 'b';
// 文字列形式から構造体への変換
MemberCount3 m3{};
apply(m3, [&]<class T>(T& value, std::string_view name) {
value = std::any_cast<T>(map[name]);
});
std::println("{},{},{}", m3.a, m3.b, m3.c);
return 0;
}
まとめ
- 闇の力を使うことで、集成体な構造体からメンバ変数名を無理やり取得できる
- 静的リフレクション早く使いたい