C++20 黑魔法之——
编译期获取聚合类字段名/成员变量名

  • ~17.58K 字
  • 次阅读
  • 条评论
  1. 1. 获取函数签名,提取字段名。
  2. 2. 通过结构化绑定,分离各个字段。
  3. 3. 利用宏生成兼容不同字段数量的代码
  4. 4. 获取字段数量
  5. 5. 完整代码
  6. 6. 修修虫子
  7. 7. 结语

本文所有代码可在这里找到

C++20 起,我们可以这样获取聚合类字段名:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>
#include <format>

template <auto ptr>
constexpr auto getFieldName()
{
return __PRETTY_FUNCTION__;
}

struct foo
{
int get_me;
float or_me;
};

int main(int, char **)
{
static foo foov;
auto &[a, b] = foov;
std::cout << std::format("{}\n{}\n", getFieldName<&a>(), getFieldName<&b>());
}

ab 是通过结构化绑定得到的对 foov 成员的引用,而 foov 是静态存储期对象。
此时将 a,b 的指针直接作为 getFieldName() 的模板实参,则 getFieldName() 的函数签名就会包含对应字段名。
以上代码在 gcc14 的输出:

1
2
constexpr auto getFieldName() [with auto ptr = (& foov.foo::get_me)]
constexpr auto getFieldName() [with auto ptr = (& foov.foo::or_me)]

可以发现 get_meor_me 出现在了签名中

总的来看,我们需要 3 个东西:

  1. 获取函数签名的宏。
  2. 结构化绑定。
  3. 静态存储期的对象。

获取函数签名,提取字段名。

3 大编译器生成的签名格式有所不同,写点测试代码观察签名规律后用 substr 裁剪即可。

1
2
3
"constexpr auto getFieldName() [with auto ptr = (& foov.foo::get_me)]" // gcc
"auto getFieldName() [ptr = &foov.get_me]" // clang
"auto getFieldName<& foov->get_me>()" // msvc
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <string_view>

#if !defined(__PRETTY_FUNCTION__) && !defined(__GNUC__)
# define __PRETTY_FUNCTION__ __FUNCSIG__
#endif

template <auto ptr>
constexpr auto getFieldName()
{
constexpr std::string_view prettyName = __PRETTY_FUNCTION__;
#if defined(__clang__)
return prettyName.substr(0, prettyName.size() - 1).substr(prettyName.find_last_of(".") + 1);
#elif defined(__GNUC__)
return prettyName.substr(0, prettyName.size() - 2).substr(prettyName.find_last_of(":") + 1);
#elif defined(_MSC_VER)
return prettyName.substr(0, prettyName.size() - 7).substr(prettyName.find_last_of("-") + 2);
#else
static_assert(false, "Unsupported compiler.");
#endif
}

以上代码参考了这篇知乎回答里的这份代码

通过结构化绑定,分离各个字段。

首先写一个 DeclVal 类,用于获取静态存储期对象,然后以 2 个字段为例,写一个小型样例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <iostream>
#include <format>
#include <array>
#include <type_traits>

struct foo
{
int get_me;
float or_me;
};

template <typename T>
struct DeclVal
{
static std::remove_cvref_t<T> value;
};

constexpr std::array<std::string_view, 2> get()
{
auto &[i1, i2] = DeclVal<foo>::value; // 结构化绑定
return {getFieldName<&i1>(), getFieldName<&i2>()}; // 取指针
};

int main()
{
constexpr auto names = get();
std::cout << std::format("{}, {}\n", names[0], names[1]);
}

以上代码输出:

1
get_me, or_me

现在引入模板,由于函数不能模板特化,所以我们将函数放入一个名为 GetFieldNamesImpl 类中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
template <typename T, std::size_t N>
struct GetFieldNamesImpl;

template <typename T>
struct GetFieldNamesImpl<T, 0>
{
static constexpr std::array<std::string_view, 0> get(std::index_sequence<0>) { return {}; };
};

template <typename T>
struct GetFieldNamesImpl<T, 2>
{
static constexpr std::array<std::string_view, 2> get()
{
auto &[i1, i2] = DeclVal<T>::value;
return {getFieldName<&i1>(), getFieldName<&i2>()};
};
};

此时要获取 foo 的字段名则需这样:

1
constexpr auto names = GetFieldNamesImpl<foo, 2>::get();

其中第二个模板参数为 foo 的成员字段数量。
此时我们可以手动特化一堆 struct GetFieldNamesImpl<T, N> 来兼容不同字段数量的类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
template <typename T>
struct GetFieldNamesImpl<T, 3>
{
static constexpr std::array<std::string_view, 3> get()
{
auto &[i1, i2, i3] = DeclVal<T>::value;
return {getFieldName<&i1>(), getFieldName<&i2>(), getFieldName<&i3>()};
};
};

template <typename T>
struct GetFieldNamesImpl<T, 4>
{
static constexpr std::array<std::string_view, 4> get()
{
auto &[i1, i2, i3, i4] = DeclVal<T>::value;
return {getFieldName<&i1>(), getFieldName<&i2>(), getFieldName<&i3>(), getFieldName<&i4>()};
};
};

但是手动写很麻烦,我们需要自动化。

利用宏生成兼容不同字段数量的代码

将其定义为宏,并用参数包代替初始化列表:

1
2
3
4
5
6
7
8
9
10
#define GEN_REFLECT_GET_FIELD_NAMES(N, ...)                                     \
template <typename T> \
struct GetFieldNamesImpl<T, N> \
{ \
static constexpr auto get() \
{ \
auto &[__VA_ARGS__] = DeclVal<T>::value; /* __VA_ARGS__取出参数包 */ \
return /* 取指针 */; \
}; \
};

然后用脚本生成一大堆这个宏的调用:

1
2
3
4
5
6
7
8
9
10
11
GEN_REFLECT_GET_FIELD_NAMES(1, i1)
GEN_REFLECT_GET_FIELD_NAMES(2, i1, i2)
GEN_REFLECT_GET_FIELD_NAMES(3, i1, i2, i3)
GEN_REFLECT_GET_FIELD_NAMES(4, i1, i2, i3, i4)
GEN_REFLECT_GET_FIELD_NAMES(5, i1, i2, i3, i4, i5)
GEN_REFLECT_GET_FIELD_NAMES(6, i1, i2, i3, i4, i5, i6)
GEN_REFLECT_GET_FIELD_NAMES(7, i1, i2, i3, i4, i5, i6, i7)
GEN_REFLECT_GET_FIELD_NAMES(8, i1, i2, i3, i4, i5, i6, i7, i8)
GEN_REFLECT_GET_FIELD_NAMES(9, i1, i2, i3, i4, i5, i6, i7, i8, i9)
// ...
GEN_REFLECT_GET_FIELD_NAMES(128, i1, i2, i3, i4, i5, i6, i7, i8, i9, i10, i11, i12, i13, i14, i15, i16, ...)

再来实现取指针部分。
首先要强调一点,getFieldName<&iN>() 要求 &iN 是常量表达式,也就要求 iN 是常量表达式。
但是结构化绑定在 C++26 才支持声明为 constexpr,因此开头的代码实际上不应通过编译,但是 gcc 和 clang 可能是做了扩展也可能是 bug,可以正常运行:

1
2
3
static foo foov;
auto &[a, b] = foov; // 结构化绑定
getFieldName<&a>(); // 取指针,原则上a不是常量表达式,但是gcc和clang可以通过编译

一种解决方法是将 getFieldName<&iN>() 的调用转到外部,比如再封装一层叫 getFieldNames() 的函数,大概像这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
template <typename T>
struct GetFieldNamesImpl<T, 1>
{
static constexpr auto get()
{
auto &[i1] = DeclVal<T>::value;
return i1;
};
};

template <typename T>
constexpr auto getFieldNames()
{
// 虽然get()内i1不是常量表达式,但是get()声明为constexpr,我们依然可以拿到常量表达式的指针
constexpr auto member = GetFieldNamesImpl<T, 1>::get();
return getFieldName<&member>();
}

但是也有更优雅的方法,那就是嵌套一个 lambda 表达式

1
2
3
4
5
6
7
8
9
10
11
12
template <typename T>
struct GetFieldNamesImpl<T, 1>
{
static constexpr auto get()
{
constexpr auto member = []() {
auto &[i1] = DeclVal<T>::value;
return i1;
}();
return getFieldName<&member>();
};
};

当然,要支持多个不同类型的字段,我们得使用 std::tuple 作为 lambda 的返回类型.
利用 std::tie(),修改代码,得到:

1
2
3
4
5
6
7
8
9
10
11
12
13
#define GEN_REFLECT_GET_FIELD_NAMES(N, ...)                    \
template <typename T> \
struct GetFieldNamesImpl<T, N> \
{ \
static constexpr std::array<std::string_view, N> get() \
{ \
constexpr auto members = []() { \
auto &[__VA_ARGS__] = DeclVal<T>::value; \
return std::tie(__VA_ARGS__); \
}(); \
return /* ??? */; \
}; \
};

以上代码通过 std::tiei1, i2, i3, … 转为 std::tuple;

而调用 lambda 得到的 members 自然是一个 tuple,包含 N 个元素,N 是字段数量;
而取出里面的元素需要使用 std::get(),如:

1
constexpr auto name = getFieldName<&std::get<0>(ptrs)>();

而要取出所有元素,我们需要一个模板参数包 std::size_t… Idx,参数包包含 N 个数字,
然后我们利用参数包展开,取出所有元素:

1
constexpr std::array</*...*/> name = {getFieldName<&std::get<Idx>(ptrs)>()...};

最终的实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#define GEN_REFLECT_GET_FIELD_NAMES(N, ...)                                               \
template <typename T> \
struct GetFieldNamesImpl<T, N> \
{ \
template <std::size_t... Idx> \
static constexpr std::array<std::string_view, N> get(std::index_sequence<Idx...>) \
{ \
constexpr auto members = []() { \
auto &[__VA_ARGS__] = DeclVal<T>::value; \
return std::tie(__VA_ARGS__); \
}(); \
return {getFieldName<&std::get<Idx>(members)>()...}; \
}; \
};

template <typename T, std::size_t N>
constexpr auto getFieldNames()
{
using T_ = std::remove_cvref_t<T>;
return GetFieldNamesImpl<T, N>::get(std::make_index_sequence<N>{});
}

第二个函数 getFieldNames() 中的 std::make_index_sequence{} 用于生成参数包。
N 是字段数量。到现在我们就可以这样获取字段名:

1
2
3
4
5
int main()
{
constexpr auto names = getFieldNames<foo, 2>();
std::cout << std::format("{}, {}\n", names[0], names[1]);
}

获取字段数量

目前我们还有最后一个问题:如何自动获取字段数量?

这又是一个大坑,且比上面的代码还要复杂。所幸前人已经为我们铺好了道路
参考这篇文章,我成功搓出来了一个结构体字段计数器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
// StructCounter.hpp
#pragma once

#include <utility>

struct StructCounter
{
template <typename T>
requires(std::is_aggregate_v<std::remove_cvref_t<T>>)
static constexpr std::size_t count()
{
using T_ = std::remove_cvref_t<T>;
constexpr std::size_t count = _count<T_>();
return count - countArray<T_, count>();
}

private:
struct Count
{
size_t c;

template <typename T>
requires(!std::is_copy_constructible_v<T> && !std::is_move_constructible_v<T>)
constexpr operator T() const;

template <typename T>
requires std::is_copy_constructible_v<T>
constexpr operator T &() const;

template <typename T>
requires std::is_move_constructible_v<T>
constexpr operator T &&() const;
};

template <typename T>
static constexpr std::size_t countAll()
{
using T_ = std::remove_cvref_t<T>;
return _count<T_>();
}

template <typename T, bool Last = false>
static constexpr std::size_t _count(auto &&...args)
{
constexpr bool isConstructible = requires { T{args...}; };
if constexpr (!isConstructible && Last)
return sizeof...(args) - 1;
else if constexpr (sizeof...(args) < 128)
return _count<T, isConstructible>(args..., Count{});
else
static_assert(false, "Too many fields");
}

template <typename T, size_t Total>
static constexpr std::size_t countArray()
{
return []<std::size_t... Before>(std::index_sequence<Before...>) {
std::size_t count = 0;
std::size_t nextPos = 0;
((count += testArrayAt<T, Total, Before>(nextPos)), ...);
return count;
}(std::make_index_sequence<Total - 1>{});
}

template <typename T, std::size_t Total, std::size_t Pos>
static constexpr std::size_t testArrayAt(std::size_t &nextPos)
{
return testArrayAt<T, Total>(std::make_index_sequence<Pos>{},
std::make_index_sequence<2>{},
std::make_index_sequence<Total - 2 - Pos>{}, nextPos);
}

template <typename T, std::size_t Total, std::size_t... Before, std::size_t... Array, std::size_t... After>
static constexpr std::size_t testArrayAt(std::index_sequence<Before...>,
std::index_sequence<Array...>,
std::index_sequence<After...>,
std::size_t &nextPos,
std::size_t count = 0)
{
// clang-format off
constexpr bool ctorAsArray = requires { T{Count{Before}..., {Count{Array}...}, Count{After}...}; };
constexpr bool ctorAsStruct = requires { T{Count{Before}..., {Count{Array}...}, Count{}, Count{After}...}; };
// clang-format on

if (sizeof...(Before) != nextPos)
return 0;
if constexpr (ctorAsArray && !ctorAsStruct)
{
nextPos = sizeof...(Before) + sizeof...(Array) - 1;
return count + sizeof...(Array) - 1;
}
else if constexpr (sizeof...(After) > 0)
{
return testArrayAt<T, Total>(std::make_index_sequence<sizeof...(Before)>{},
std::make_index_sequence<sizeof...(Array) + 1>{},
std::make_index_sequence<Total - 1 - sizeof...(Before) - sizeof...(Array)>{},
nextPos, count);
}
else
{
++nextPos;
return count;
}
}
};

使用示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct A
{
int x;
int y;
int z;
};

struct foo3
{
int y[2];
int m;
A c;
int a[2][6];
};

constexpr auto c = StructCounter::count<foo3>(); // 4

完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63

#include <string_view>
#include <array>

#include "StructCounter.hpp"

#if !defined(__PRETTY_FUNCTION__) && !defined(__GNUC__)
# define __PRETTY_FUNCTION__ __FUNCSIG__
#endif

template <auto ptr>
constexpr auto getFieldName()
{
constexpr std::string_view prettyName = __PRETTY_FUNCTION__;
#if defined(__clang__)
return prettyName.substr(0, prettyName.size() - 1).substr(prettyName.find_last_of(".") + 1);
#elif defined(__GNUC__)
return prettyName.substr(0, prettyName.size() - 2).substr(prettyName.find_last_of(":") + 1);
#elif defined(_MSC_VER)
return prettyName.substr(0, prettyName.size() - 7).substr(prettyName.find_last_of("-") + 2);
#else
static_assert(false, "Unsupported compiler.");
#endif
}

template <typename T>
struct DeclVal
{
static std::remove_cvref_t<T> value;
};

template <typename T, std::size_t N>
struct GetFieldNamesImpl;

#define GEN_REFLECT_GET_FIELD_NAMES(N, ...) \
template <typename T> \
struct GetFieldNamesImpl<T, N> \
{ \
template <std::size_t... Idx> \
static constexpr std::array<std::string_view, N> get(std::index_sequence<Idx...>) \
{ \
constexpr auto members = []() { \
auto &[__VA_ARGS__] = DeclVal<T>::value; \
return std::tie(__VA_ARGS__); \
}(); \
return {getFieldName<&std::get<Idx>(members)>()...}; \
}; \
};

GEN_REFLECT_GET_FIELD_NAMES(1, i1)
GEN_REFLECT_GET_FIELD_NAMES(2, i1, i2)
GEN_REFLECT_GET_FIELD_NAMES(3, i1, i2, i3)
GEN_REFLECT_GET_FIELD_NAMES(4, i1, i2, i3, i4)
// ...

template <typename T>
requires(std::is_aggregate_v<std::remove_cvref_t<T>>)
constexpr auto getFieldNames()
{
using T_ = std::remove_cvref_t<T>;
constexpr std::size_t count = StructCounter::count<T_>();
return GetFieldNamesImpl<T, count>::get(std::make_index_sequence<count>{});
}

修修虫子

msvc 作为与众不同的编译器,总是会在莫名其妙的地方出现一些诡异的行为。

在本实现中,如果有两个结构体间存在重名的成员变量,且对两个结构体均进行的字段名反射,则 msvc 生成结果有误:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct Bar
{
int x;
int hello_Foo_2nd_field_name;
};

struct Foo
{
int x;
int m;
};

int main()
{
constexpr auto bar = FieldNames::getFieldNames<Bar>();
constexpr auto foo = FieldNames::getFieldNames<Foo>();
std::cout << std::format("{} {}\n", bar[0], bar[1]);
std::cout << std::format("{} {}\n", foo[0], foo[1]);
}

msvc 下的输出:

1
2
x hello_Foo_2nd_field_name
x hello_Foo_2nd_field_name

可见反射 Bar 得到的元信息覆盖了后者。

交换两行反射代码,则同样反射 Foo 得到的元信息会覆盖另一个:

1
2
x m
x m

且删掉其中一个反射实例,另一个结果正确…


分析原因,可能是 getFieldName() 函数在字段重名的情况下实例化,msvc 认为函数签名或函数符号重整名是相同的。

1
2
template <auto ptr> // ptr指向重名的字段,msvc认为是同一个实例化的函数
constexpr auto getFieldName();

那么我们就有解决思路了,添加一个指向结构体类型的模板参数,保证签名不同:

1
2
template <typename T, auto ptr>
constexpr auto getFieldName();

结语

结什么语?没有结语。

分享这一刻
让朋友们也来瞅瞅!