C++17
基于结构绑定
的编译期反射
事实上不需要宏的编译期反射
在C++17
中已用得很多了,比如struct_pack
的编译期反射
就不需要宏,因为C++17结构绑定
可直接得到一个聚集
类的成员的引用.
cpp
struct person {
int id;
std::string name;
int age;
};
int main() {
person p{1, "tom", 20};
auto &[id, name, age] = p;
std::cout << name << "\n";
}
没有宏也没有侵入式
,一切都很完美,但是有两个比较大的问题:
问题一
结构绑定
方式无法取字段名
,这是一个主要问题,如果想序化对象到json,xml
时,需要字段名
时就无法满足需求了.
问题二
除此外还有另外一个问题,如果一个对象有构造器
或私有成员
时,则它就不是一个聚集
类型了,无法再用结构绑定
去反射
内部的字段了.
基于结构绑定
的编译期反射
无法解决这两个主要问题,导致用途不大.
yalantinglibs.reflection
反射库
yalantinglibs.reflection
反射库直面并解决了这两个问题,提供了统一的编译期反射方法
,无论对象是否是聚集
类型,无论对象是否有私有字段
都可用统一的api
在编译期反射
得到其元信息
.
来看看yalantinglibs.reflection
如何反射一个聚集
类型的:
cpp
struct simple {
int color;
int id;
std::string str;
int age;
};
using namespace ylt::reflection;
int main() {
simple p{.color = 2, .id = 10, .str = "hello reflection", .age = 6};
//取对象字段个数
static_assert(members_count_v<simple> == 4);
//取对象所有字段名
constexpr auto arr = member_names<simple>; //std::array<std::string_view, N>
//根据字段索引取字段值
CHECK(std::get<3> == 6); //get age
CHECK(std::get<2> == "hello reflection"); //get str
//根据字段名取字段值
auto& age2 = get<"age"_ylts>(p);
CHECK(age2 == 6);
//遍历对象,得到字段值和字段名
for_each(p, [](auto& field_value, auto field_name) {
std::cout << field_value << ", " << field_name << "\n";
});
}
yalantinglibs
的编译期反射相比前结构绑定
方式的反射
更进一步了,不仅是无宏非侵入式
,还能得到字段名
,这样就把第一个问题解决掉了,可用在数格
和xml
等需要字段名
的场景下了.
yalantinglibs.reflection
是如何完成非侵入式
取得聚集
对象的字段名
的呢?因为reflectcpp
这道光!
该库的作者发现了一个新方法可在C++20
高版本的编译器中非侵入式的取得聚集
对象的字段名.能发现该方法我只能说你真是个天才
!
该方法说起来也不算复杂,分两步实现:
第一步:在编译期取得对象字段值的指针
;
第二步:在编译期第一步得到的指针解析出字段名
;
是不是很简单,接着看看具体是如何实现的吧.
在编译期取得对象字段值的指针
reflectcpp
在实现这一步时做得比较复杂,yalantinglibs
大幅简化了这一步的实现.
cpp
template <class T, std::size_t n>
struct object_tuple_view_helper {
static constexpr auto tuple_view(){}
};
template <class T>
struct object_tuple_view_helper<T, 0> {
static constexpr auto tuple_view() { return std::tie(); }
};
template <class T>
struct object_tuple_view_helper<T, 4> {
static constexpr auto tuple_view() {
auto& [a, b, c, d] = get_fake_object<remove_cvref_t<T>>();
auto ref_tup = std::tie(a, b, c, d);
auto get_ptrs = [](auto&... _refs) {
return std::make_tuple(&_refs...);
};
return std::apply(get_ptrs, ref_tup);
}
};
偏特化
模板类object_tuple_view_helper
,在tuple_view
函数中,先结构绑定
得到字段值的引用
,然后按指针转换它
,并放到元组
中,来返回给用户.
这里偏特化
的关键在于n
,它表示聚集
对象字段的个数,该字段个数是可在编译期
取的.为了避免针对不同字段个数
的聚集
类型写重复的偏特化的代码
,可用脚本生成这些代码
.
cpp
#define RFL_INTERNAL_OBJECT_IF_YOU_SEE_AN_ERROR_REFER_TO_DOCUMENTATION_ON_C_ARRAYS( \
n, ...) \
template <class T> \
struct object_tuple_view_helper<T, n> { \
static constexpr auto tuple_view() { \
auto& [__VA_ARGS__] = get_fake_object<remove_cvref_t<T>>(); \
auto ref_tup = std::tie(__VA_ARGS__); \
auto get_ptrs = [](auto&... _refs) { \
return std::make_tuple(&_refs...); \
}; \
return std::apply(get_ptrs, ref_tup); \
} \
}
/*The following boilerplate code was generated using a Python script:
macro =
"RFL_INTERNAL_OBJECT_IF_YOU_SEE_AN_ERROR_REFER_TO_DOCUMENTATION_ON_C_ARRAYS"
with open("generated_code4.cpp", "w", encoding="utf-8") as codefile:
codefile.write(
"\n".join(
[
f"{macro}({i}, {', '.join([f'f{j}' for j in range(i)])});"
for i in range(1, 256)
]
)
)
*/
RFL_INTERNAL_OBJECT_IF_YOU_SEE_AN_ERROR_REFER_TO_DOCUMENTATION_ON_C_ARRAYS(1, f0);
RFL_INTERNAL_OBJECT_IF_YOU_SEE_AN_ERROR_REFER_TO_DOCUMENTATION_ON_C_ARRAYS(2, f0, f1);
RFL_INTERNAL_OBJECT_IF_YOU_SEE_AN_ERROR_REFER_TO_DOCUMENTATION_ON_C_ARRAYS(3, f0, f1, f2);
RFL_INTERNAL_OBJECT_IF_YOU_SEE_AN_ERROR_REFER_TO_DOCUMENTATION_ON_C_ARRAYS( 4, f0, f1, f2, f3);
RFL_INTERNAL_OBJECT_IF_YOU_SEE_AN_ERROR_REFER_TO_DOCUMENTATION_ON_C_ARRAYS( 5, f0, f1, f2, f3, f4);
RFL_INTERNAL_OBJECT_IF_YOU_SEE_AN_ERROR_REFER_TO_DOCUMENTATION_ON_C_ARRAYS( 6, f0, f1, f2, f3, f4, f5);
...
生成偏特化
代码后,就可简单取得聚集
对象字段的指针
.
cpp
template <class T>
inline constexpr auto struct_to_tuple() {
return object_tuple_view_helper<T, members_count_v<T>>::tuple_view();
}
调用struct_to_tuple
就能在编译期取得T所有字段的指针了,其中编译期取T的字段个数的方法members_count_v
就是来自于struct_pack
中的方法,前面在介绍struct_pack
的文章里已详细讲过,就不再赘述了.
根据字段指针取字段名
有了编译期得到的字段指针
后就很容易取其字段名
了:
cpp
template <auto ptr>
inline constexpr std::string_view get_member_name() {
#if defined(_MSC_VER)
constexpr std::string_view func_name = __FUNCSIG__;
#else
constexpr std::string_view func_name = __PRETTY_FUNCTION__;
#endif
#if defined(__clang__)
auto split = func_name.substr(0, func_name.size() - 2);
return split.substr(split.find_last_of(":.") + 1);
#elif defined(__GNUC__)
auto split = func_name.substr(0, func_name.rfind(")}"));
return split.substr(split.find_last_of(":") + 1);
#elif defined(_MSC_VER)
auto split = func_name.substr(0, func_name.rfind("}>"));
return split.substr(split.rfind("->") + 2);
#else
static_assert(false,
"You are using an unsupported compiler. Please use GCC, Clang "
"or MSVC or switch to the rfl::Fieldsyntax.");
#endif
}
template<auto ptr>
是C++17
的特性,可用动
来声明一个非类型模板参数
,来避免写具体类型
.
有了该编译期的针
后,剩下的就是根据编译产生的符号
去截取需要的部分串
了,注意每个平台生成的符号有差异,需要宏来区分各个平台的截取方式
.
为什么用指针
可以取字段名
?C++17
或以下的编译器是不是也可这样来取呢?
第一个问题的答案是:reflectcpp
作者发现的该方法,很黑客
,但是工作
!
第二个问题的答案是:不可以
,该方法只在支持C++20
的gcc11,clang13,msvc2022
以上编译器中才有效!
所以该非侵入式取字段名
的方法也是有约束的,不适合低版本
的编译器.
完整的取字段列表的实现:
cpp
template <class T>
struct Wrapper {
using Type = T;
T v;
};
template <class T>
Wrapper(T) -> Wrapper<T>;
//针对clang.
template <class T>
inline constexpr auto wrap(const T& arg) noexcept {
return Wrapper{arg};
}
template <typename T>
inline constexpr std::array<std::string_view, members_count_v<T>>
get_member_names() {
constexpr auto tp = struct_to_tuple<T>();
std::array<std::string_view, Count> arr;
[&]<size_t... Is>(std::index_sequence<Is...>) mutable {
((arr[Is] = get_member_name<wrap(std::get<Is>(tp))>()), ...);
}
(std::make_index_sequence<Count>{});
return arr;
}
至此,两步完成,可用get_member_names
函数非侵入式
的取得聚集
对象的字段名列表
了.
如何处理非聚集
类型?
在高版本的编译器中无宏非侵入式
得到聚集
对象字段名列表固然很好,但是非聚集
类型要如何处理呢?如果编译器版本不够,只有C++17
又该怎么办?
yalantinglibs.reflection
的未来
远不止你想象的,想能统一整个编译期反射
的内容,无论对象是聚集
还是非聚集
,无论对象
是否含有私有字段
都提供统一的反射接口
!
比如像这样一个对象:
cpp
class private_struct {
int a;
int b;
public:
private_struct(int x, int y) : a(x), b(y) {}
};
private_struct st(2, 4);
ylt::reflection::refl_visit_members(st, [](auto&... args) {
((std::cout << args << " "), ...);
std::cout << "\n";
});
private_struct
是一个含私有字段
的非聚集
类型,但是ylt::reflection
也能反射
它.但是这里还漏掉了一个宏,是,还是需要宏,在编译期反射进入到标准库前,对非聚集
类型仍需要的.
cpp
class private_struct {
int a;
int b;
public:
private_struct(int x, int y) : a(x), b(y) {}
};
YLT_REFL_PRIVATE(private_struct, a, b);
对没有私
字段的非聚集
类型来说,也适合C++17
,可这样定义宏:
cpp
struct dummy_t {
int id;
std::string name;
int age;
YLT_REFL(dummy_t, id, name, age);
};
struct dummy_t2 {
int id;
std::string name;
int age;
};
YLT_REFL(dummy_t2, id, name, age);
总结
高版本的编译器中可完全不使用宏
,低版本或非聚集
类型宏来实现编译器反射,无论是聚集
还是非聚集
都是使用同一套反射接口
,这样可覆盖所有
要用编译期反射的场景,这就是yalantinglibs.reflection
提供的能力!
后面struct_pack,struct_pb,struct_json,struct_xml,struct_yaml
都会使用yalantinglibs.reflection
提供的统一的编译期反射接口
来实现序化和反序化
.