iguana
Github : https://github.com/fananchong/iguana
官方介绍: universal serialization engine
虽然官方介绍是通用的序列化引擎,但实际上目前只支持:
- json
- yaml
- xml
不过, C++ 结构体/类的反射部分是通用的
通过该库,可以学习到使用宏和模板实现 C++ 反射的一种方法
iguana 用法示例
先简单看下 iguana 如何实现:
以下代码摘自 https://github.com/qicosmos/iguana?tab=readme-ov-file#tutorial
定义 person :
struct person
{std::string name;int age;
};
REFLECTION(person, name, age) //define meta data
序列化为 json 字符串:
person p = { "tom", 28 };
iguana::string_stream ss; // here use std::string is also ok
iguana::to_json(p, ss);
std::cout << ss.str() << std::endl;
从 json 字符串,反序列化回 person
std::string json = "{ \"name\" : \"tom\", \"age\" : 28}";
person p;
iguana::from_json(p, json);
以上例子中,通过REFLECTION(person, name, age)
,在编译期,反射 person 相关字段信息
运行态,可以利用这些反射的信息,做to_json
和from_json
REFLECTION 宏分析
REFLECTION 宏定义如下:
#define REFLECTION(STRUCT_NAME, ...) \MAKE_META_DATA(STRUCT_NAME, #STRUCT_NAME, GET_ARG_COUNT(__VA_ARGS__), \__VA_ARGS__)#define MAKE_META_DATA(STRUCT_NAME, TABLE_NAME, N, ...) \static constexpr inline std::array<frozen::string, N> arr_##STRUCT_NAME = { \MARCO_EXPAND(MACRO_CONCAT(CON_STR, N)(__VA_ARGS__))}; \static constexpr inline std::string_view fields_##STRUCT_NAME = { \MAKE_NAMES(__VA_ARGS__)}; \static constexpr inline std::string_view name_##STRUCT_NAME = TABLE_NAME; \MAKE_META_DATA_IMPL(STRUCT_NAME, \MAKE_ARG_LIST(N, &STRUCT_NAME::FIELD, __VA_ARGS__))#define MAKE_META_DATA_IMPL(STRUCT_NAME, ...) \[[maybe_unused]] inline static auto iguana_reflect_members( \STRUCT_NAME const &) { \struct reflect_members { \constexpr decltype(auto) static apply_impl() { \return std::make_tuple(__VA_ARGS__); \} \using size_type = \std::integral_constant<size_t, GET_ARG_COUNT(__VA_ARGS__)>; \constexpr static std::string_view name() { return name_##STRUCT_NAME; } \constexpr static std::string_view struct_name() { \return std::string_view(#STRUCT_NAME, sizeof(#STRUCT_NAME) - 1); \} \constexpr static std::string_view fields() { \return fields_##STRUCT_NAME; \} \constexpr static size_t value() { return size_type::value; } \constexpr static std::array<frozen::string, size_type::value> arr() { \return arr_##STRUCT_NAME; \} \}; \return reflect_members{}; \}
把REFLECTION(person, name, age)
展开:
static constexpr inline std::array<frozen::string, 2> arr_person = {std::string_view("name", sizeof("name") - 1),std::string_view("age", sizeof("age") - 1),
};
static constexpr inline std::string_view fields_person = {"name, age",
};
static constexpr inline std::string_view name_person = "person";
[[maybe_unused]] inline static auto iguana_reflect_members(person const &) {struct reflect_members {constexpr decltype(auto) static apply_impl() {return std::make_tuple(&person::name, &person::age);}using size_type = std::integral_constant<size_t, 2>;constexpr static std::string_view name() { return name_person; }constexpr static std::string_view struct_name() {return std::string_view("person", sizeof("person") - 1);}constexpr static std::string_view fields() { return fields_person; }constexpr static size_t value() { return size_type::value; }constexpr static std::array<frozen::string, size_type::value> arr() {return arr_person;}};return reflect_members{};
}
从宏展开代码可以看到, REFLECTION 定义可以得到 person 的以下元信息:
元信息的宏定义 | person | 说明 |
---|---|---|
arr_##STRUCT_NAME | arr_person | 字段名列表,类型为 std::array<frozen::string, 2> |
fields_##STRUCT_NAME | fields_person | 字段名列表,类型为 std::string_view |
name_##STRUCT_NAME | name_person | 结构体/类名 |
iguana_reflect_members(STRUCT_NAME const &){} | iguana_reflect_members(person const &) | 元数据信息。通过调用 iguana_reflect_members 返回 reflect_members 结构体 |
reflect_members 元数据结构体,除了上面表格中列的内容,还提供了:
方法 | 元数据 | 说明 |
---|---|---|
apply_impl() | 字段地址列表,类型 std::tuple | 实现反射的关键。通过它结合结构体/类实例,获取结构体/类实例字段的值或赋值 |
value() | 字段个数 |
from_json 实现
from_json 函数实现在iguana/json_reader.hpp
, 504 行 - 599 行
把一些边界代码、遍历代码去掉,核心逻辑如下:
std::string_view key = detail::get_key(it, end);static constexpr auto frozen_map = get_iguana_struct_map<T>();const auto &member_it = frozen_map.find(key);std::visit([&](auto &&member_ptr) IGUANA__INLINE_LAMBDA {from_json_impl(value.*member_ptr, it, end);},member_it->second);
这段代码的意思是:
代码 | 说明 |
---|---|
it, end | it 指向当前解析到 json 字符串的位置; end json 串结尾位置 |
key = detail::get_key(it, end) | 获取字段名 |
frozen_map = get_iguana_struct_map<T>() | 获取一个 map ,该 map key 为字段名;值为 std::variant 类型的字段地址 上面提到 apply_impl() 返回字段地址列表,类型 std::tuple get_iguana_struct_map 函数就是把 std::tuple 类型的内容,编译期转成 std::variant 类型 |
std::visit | 对 std::variant 类型对象做访问 |
from_json_impl(value.*member_ptr, it, end) | from_json_impl 是个模板,不同类型都有特化实现 it, end 获得 value 值,转化为对应类型赋值 value.*member_ptr |
from_json 过程思路很清晰,就是把 key - value (字段,字段值),填充到对象上
从 apply_impl() 得到的字段地址列表,实际上已经可以实现这个思路
iguana 在实现上,考虑到编码的简洁,引入了 std::visit - std::variant 编程技巧
对每种类型的解析赋值过程,均对应一个 from_json_impl 类型特化的模板函数
这样就不会有 if else 颓长的类型判断代码
同时,成员字段也可能是需要反射的类型,那么 from_json_impl 类型特化的模板函数也实现一个,就可以实现递归解析了:
template <typename U, typename It, std::enable_if_t<refletable_v<U>, int> = 0>
IGUANA_INLINE void from_json_impl(U &value, It &&it, It &&end) {from_json(value, it, end);
}
再举例解析 bool 类型值如下:
template <typename U, typename It, std::enable_if_t<bool_v<U>, int> = 0>
IGUANA_INLINE void from_json_impl(U &&value, It &&it, It &&end) {skip_ws(it, end);if (it < end)IGUANA_LIKELY {switch (*it) {case 't':++it;match<'r', 'u', 'e'>(it, end);value = true;break;case 'f':++it;match<'a', 'l', 's', 'e'>(it, end);value = false;break;IGUANA_UNLIKELY default: throw std::runtime_error("Expected true or false");}}elseIGUANA_UNLIKELY { throw std::runtime_error("Expected true or false"); }
}
to_json 函数实现,思路类似,不再复述
总结
iguana 实现反射思考:
- 通过定义 REFLECTION 宏,在编译期,生成结构体/类的元数据信息
- 字段名列表
- 字段地址列表
- 将字段地址列表做成 std::tuple
- 将该 std::tuple 做成 std::map , 其 key 为字段名,其值为 std::variant 类型字段地址
- 不同格式的序列化、反序列,最终要通过字段名给对象的字段赋值或取值
- 通过 std::visit - std::variant 编程技巧
- 使用函数类型特化方式,避免 if else 这种类型分支判断
以上