本文所有代码可在这里找到
C++20 起,我们可以这样获取聚合类字段名:
1 |
|
a 和 b 是通过结构化绑定得到的对 foov 成员的引用,而 foov 是静态存储期对象。
此时将 a,b 的指针直接作为 getFieldName() 的模板实参,则 getFieldName() 的函数签名就会包含对应字段名。
以上代码在 gcc14 的输出:
1 | constexpr auto getFieldName() [with auto ptr = (& foov.foo::get_me)] |
可以发现 get_me 和 or_me 出现在了签名中
总的来看,我们需要 3 个东西:
- 获取函数签名的宏。
- 结构化绑定。
- 静态存储期的对象。
获取函数签名,提取字段名。
3 大编译器生成的签名格式有所不同,写点测试代码观察签名规律后用 substr 裁剪即可。
1 | "constexpr auto getFieldName() [with auto ptr = (& foov.foo::get_me)]" // gcc |
1 |
|
通过结构化绑定,分离各个字段。
首先写一个 DeclVal 类,用于获取静态存储期对象,然后以 2 个字段为例,写一个小型样例:
1 |
|
以上代码输出:
1 | get_me, or_me |
现在引入模板,由于函数不能模板特化,所以我们将函数放入一个名为 GetFieldNamesImpl 类中:
1 | template <typename T, std::size_t N> |
此时要获取 foo 的字段名则需这样:
1 | constexpr auto names = GetFieldNamesImpl<foo, 2>::get(); |
其中第二个模板参数为 foo 的成员字段数量。
此时我们可以手动特化一堆 struct GetFieldNamesImpl<T, N> 来兼容不同字段数量的类:
1 | template <typename T> |
但是手动写很麻烦,我们需要自动化。
利用宏生成兼容不同字段数量的代码
将其定义为宏,并用参数包代替初始化列表:
1 |
然后用脚本生成一大堆这个宏的调用:
1 | GEN_REFLECT_GET_FIELD_NAMES(1, i1) |
再来实现取指针部分。
首先要强调一点,getFieldName<&iN>() 要求 &iN 是常量表达式,也就要求 iN 是常量表达式。
但是结构化绑定在 C++26 才支持声明为 constexpr,因此开头的代码实际上不应通过编译,但是 gcc 和 clang 可能是做了扩展也可能是 bug,可以正常运行:
1 | static foo foov; |
一种解决方法是将 getFieldName<&iN>() 的调用转到外部,比如再封装一层叫 getFieldNames() 的函数,大概像这样:
1 | template <typename T> |
但是也有更优雅的方法,那就是嵌套一个 lambda 表达式:
1 | template <typename T> |
当然,要支持多个不同类型的字段,我们得使用 std::tuple 作为 lambda 的返回类型.
利用 std::tie(),修改代码,得到:
1 |
以上代码通过 std::tie 将 i1, 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 |
|
第二个函数 getFieldNames() 中的 std::make_index_sequence
N 是字段数量。到现在我们就可以这样获取字段名:
1 | int main() |
获取字段数量
目前我们还有最后一个问题:如何自动获取字段数量?
这又是一个大坑,且比上面的代码还要复杂。所幸前人已经为我们铺好了道路,
参考这篇文章,我成功搓出来了一个结构体字段计数器:
1 | // StructCounter.hpp |
使用示例:
1 | struct A |
完整代码
1 |
|
修修虫子
msvc 作为与众不同的编译器,总是会在莫名其妙的地方出现一些诡异的行为。
在本实现中,如果有两个结构体间存在重名的成员变量,且对两个结构体均进行的字段名反射,则 msvc 生成结果有误:
1 | struct Bar |
msvc 下的输出:
1 | x hello_Foo_2nd_field_name |
可见反射 Bar 得到的元信息覆盖了后者。
交换两行反射代码,则同样反射 Foo 得到的元信息会覆盖另一个:
1 | x m |
且删掉其中一个反射实例,另一个结果正确…
分析原因,可能是 getFieldName
1 | template <auto ptr> // ptr指向重名的字段,msvc认为是同一个实例化的函数 |
那么我们就有解决思路了,添加一个指向结构体类型的模板参数,保证签名不同:
1 | template <typename T, auto ptr> |
结语
结什么语?没有结语。