现代反射实现(四):Json反序列化

第四章:基于反射的 Json 反序列化 前一章我们已经用编译期反射实现了 Json 序列化,把一个聚合类型一次性吐成一段 Json 字符串,不需要手写拼接。 这章做反方向的事情: 给定一段 Json 文本,把它自动填充进一个结构体对象里,支持嵌套结构体、顺序容器、关联容器等常见组合。 最终对外只保留一个接口: 1 2 3 4 5 6 7 8 tinyrefl::Complex obj{}; const char* json = R"({ "id": 42, "name": "tinyrefl", "values": [1, 2, 3] })"; auto st = tinyrefl::reflection_from_json(obj, json); 这里我采用不构建 DOM,直接基于RapidJSON的流式 SAX 接口,把事件派发到对应的结构体成员。 主要思路: 由于RapidJson的SAX接口只提供回调,所以对于递归反序列化,还需要我们自己维护一个解析栈来处理嵌套情况 同时,RapidJson对于数组类型和普通类型的回调方式不同,所以这里需要分别进行处理 剩下的就是使用RapidJson的接口了,RapidJson相关文档:https://rapidjson.org/zh-cn/ 结构体成员KV化 到反序列化这里,前面的铺垫已经很充分了: members_count_v<T> 能在编译期算出成员个数 struct_members_to_array<T>() 能拿到成员名数组 struct_members_to_tuple<T>() 能拿到成员引用 tuple(在有对象实例的前提下) 序列化的时候,我们只需要在“有对象”的场景下遍历一遍成员,把 (member_name, member_reference)这一对在运行时交给 to_json_value 就行,所以直接用 tuple + 泛型 lambda 遍历就够了。 反序列化就不一样了,真正要解决的是下面这个问题: 当 RapidJSON 回调告诉我「现在读到了 key = ‘id’」,如何在 O(1) 时间里找到这个成员在结构体里的“位置”和“类型”,然后把后续的值事件填到正确的成员上? 也就是说,我们需要把“结构体的成员元信息”预先组织成某种 Key → Value 的形式: ...

2025-11-15   6分钟   SSmallOrange

现代反射实现(三):Json序列化

第三章:基于反射的 Json 序列化 代码实现方案参考:https://github.com/qicosmos/iguana 通过前面的努力,我们目前已经有了以下能力: 通过编译器内置宏拿到成员名 通过结构化绑定和打表拿到成员引用 tuple 通过递归构造拿到成员数量 简单说,现在我们已经能在编译期拿到一个结构体的“成员列表”,并在运行时获得对应的引用。 有了这些能力,我们就能实现:一行代码序列化结构体对象,摒弃繁琐的Json拼装过程: 1 2 3 struct Complex obj; std::string strJson; tinyrefl::reflection_to_json(obj, strJson); // 序列化 最终支持的能力包括: 1 2 3 4 5 6 7 // 普通字段 int / double / bool / std::string / char* // 嵌套 struct std::vector / std::list / std::deque // 容器和多层嵌套容器 std::map<std::string, T> / std::unordered_map<std::string, T> vector<vector<int>> / vector<vector<T>> 实现方案 目前传统的Json支持各种类型,如:对象、数组、字符串、数字、Bool等。 类型不同,Json的输出方式也不同,比如字符串类型可能需要双引号进行包裹,数组类型或对象类型则需要括号来标识,想要将结构体序列化成一个Json格式的字符串,就需要对结构体中的各种类型分别进行处理。 做编译期类型区分,模板显然是再适合不过了。 前两章已经把“怎么拿到成员”这件事解决掉了:我们能拿到 members_count_v<T>,能拿到成员名数组,也能遍历到每个成员的引用。序列化阶段真正要解决的问题,其实只有一个: 对于任意一个 member_reference,它到底应该被当成什么类型写进 Json。 典型几种情况: 内置算术类型,按数字写 布尔类型,写成 true / false 字符串、字符指针,加引号并做转义 容器,写成数组或对象 自定义聚合类型,展开成对象并递归处理成员 在类型分发之前,首先我们需要能够遍历结构体成员。 结构体成员循环遍历 对外只暴露一行接口: 1 2 template <detail::AggregateType T, detail::OutputStream Stream> inline void reflection_to_json(T&& object, Stream &stream); 其中AggregateType是目前序列化所支持的成员类型,OutputStream是我们能够支持的输出类型,可以只限定为std::string。 1 2 3 4 5 6 7 8 9 // 检查是否为聚合类型 template <typename T> concept AggregateType = ::std::is_aggregate_v<remove_cvref_t<T>>; // 检查字符串类型 template<typename Stream> concept OutputStream = requires(Stream& s) {{ s.append("abc") };}; // 或 template<typename Stream> concept OutputStream = ::std::is_same_v<std::remove_cvref_t<Stream>, std::string>; 在第一层类型检查通过后,我们就需要对具体类型进行递归分发了,首先我们要能够循环的遍历结构体成员: ...

2025-11-15   4分钟   SSmallOrange

现代反射实现(二):元数据获取

现代反射实现(二):元数据获取 这一部分,我们就来使用上一章介绍的芝士获取结构体的元信息。 参考链接: 编译期反射成员名称 编译期获取结构体成员数量 侵入式编译期反射 结构体成员名称获取 ​ 上一章我们提到,可以通过编译器内置的宏(MSVC:__FUNCSIG__、clang、gcc:__PRETTY_FUNCTION__)来获取成员变量名称,具体表现为: 1 // void __cdecl get_func_name_template<&p.m_name>(void) 定义一个示例用的结构体:struct Person { int m_age; std::string m_name; }; 那么,通过一定的字符串截取,我们就可以拿到一个结构体成员的名称: 1 2 3 4 5 6 7 8 9 10 11 12 template <auto val> inline constexpr std::string_view get_member_type_name() { #if defined(_MSC_VER) std::string_view funcName = __FUNCSIG__; size_t begin = funcName.rfind(".") + 1; size_t end = funcName.rfind(">("); return funcName.substr(begin, end - begin); #endif } static Person p; get_func_name_template<&p.m_name>(); // out: m_name 但是,对于一个用于存储网络请求的结构体来说,结构体成员的数量可能非常多,如果对于每一个都这样手动的去获取,和REFLECTION(Person, m_name, m_age)就没区别了,这时候上一章讲到的结构化绑定就派上用场了,能够想到: ...

2025-08-12   4分钟   SSmallOrange

现代反射实现(一):语法基础

现代反射实现(一):语法基础 对于模板了解不多的同学可以先学习下模板基础,这里推荐博客:模板元编程教程 ,讲的很详细。 以下是我们在实现反射时会用到的部分语法介绍 为什么选择 C++20 首先,实现一个反射功能就需要知道一个对象的:成员名称、成员的值,并且要有修改成员值的能力,而c++因为“零运行时开销”的设计初衷,导致反射迟迟没有被纳入标准(c++26已支持),因为反射往往意味着运行时元数据,这会带来额外内存和性能开销,与零开销原则冲突。 不过我们可以通过一些其他的方式,实现我们自己的简单的反射功能,不过这肯定会不可避免的带来: 代码体积膨胀(反射类型越多,体积越大) 运行时内存增加(运行时保留类型元数据) 不过在软件工程中,复杂度是守恒的,只会转移不会消失,所以一定程度的编码便利带来一定的开销也是可以接受的。 通过查找资料发现,目前c++20的反射实现主要思路都是: 通过结构化绑定和编译器内置的宏(MSVC:__FUNCSIG__、clang、gcc:__PRETTY_FUNCTION__)来获得对象的成员名称 通过结构化绑定获得对象的成员引用(可以获得和修改对象数据) 这里的__FUNCSIG__顾名思义是获得函数签名的宏,对于如下调用: 1 2 3 4 5 6 7 8 9 10 11 template <auto val> inline void get_func_name_template() { std::cout << __FUNCSIG__ << std::endl; } struct Person { int m_age; std::string m_name; }; static Person p; get_func_name_template<&p.m_name>(); // MSVC C++20编译输出:void __cdecl get_func_name_template<&p.m_name>(void) 可以看到,输出中包含了我们需要的成员名称,对于这种固定模板,我们可以很容易的获得他的成员名称,但是如上代码在C++17是无法编译通过的,C++17需要如下格式: 1 get_func_name_template<&p>(); 原因在于:C++17对于非类型模板参数的值类型限制很严格,不允许将对象的子对象地址作为模板入参,这就直接导致了C++20以下的版本通常只能通过如下方式手动注册对象的元信息: 1 REFLECTION(Person, m_age, m_name); 这就直接导致了想要一行代码实现序列化和反序列化,只能以C++20作为基础来实现。 而其他的限制通常都可以通过SFINAE来规避,只不过实现起来会更加复杂。 同时C++20还有很多方便我们实现的特性,如:支持模板的Lambda表达式,concept和requires等,能够大大提高我们的代码可读性,提高开发效率。 语法介绍 对模板Lambda表达式的支持 在 C++20 中,你可以让 Lambda 接收模板参数,这在编译期展开成员信息时非常方便。 ...

2025-08-11   3分钟   SSmallOrange