现代反射实现(四):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 的形式: ...

November 15, 2025 · 6 min · SSmallOrange

Sol2:在C++中调用Lua脚本

Sol2:在C++中调用Lua脚本 本文档记录了在 C++ 代码中使用 sol2 嵌入 Lua 脚本时的一些个人理解。 向Lua中注册自定义用户类型 先看一段代码: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 sol::state lua; lua.new_usertype<Type>("TypeName", // 类型构造函数 sol::constructors<ConstructFunc1, ConstructFunc2, ...> // 或使用:sol::no_constructor 来禁用 TypeName.new() // 成员变量 "member1", Type.valueptr1, "member2", Type.valueptr2, // 元方法 sol::meta_function::addition, AddFun1, sol::meta_function::substraction, SubFun1, sol::meta_function::multiplication, MultiplicationFun1, // 成员方法 "memberFunc1", MemberFunc1(), "memberFunc2", [](Type& self, /* args... */) { // 对应成员方法的第一个参数 // func... }, ); 这段代码用来向lua脚本中注册一个c++类型,支持一些基本的元方法重载(比如四则运算、比大小、迭代(pair或ipair)等)。 ...

February 2, 2026 · 16 min · SSmallOrange

Hazel学习笔记:游戏功能完善(持续更新)

Hazel学习笔记:游戏功能完善 由于Hazel剩下的内容非常多,我以前也没有接触过游戏、渲染部分,所以学起来比较吃力。这部分我将作为OpenGL、游戏相关知识的积累,沉淀一些个人理解,并持续更新。 ECS ECS是游戏中常用的架构,他之所以流行,是因为:他把对象的存储和使用分开,在不影响易用性的前提下,保证了性能。 如果没有ECS,我们应该如何处理游戏对象? 假如我们现在有一个路灯和一个玩家,他们作为游戏世界的一员,首先需要有自己的位置信息,现在要让这个路灯和这个玩家都具备发光的能力,那么我们很容易能想到: 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 class Transform { public: void SetPos(vec3 pos) {} vec3 GetPos() {} private: vec3 pos; }; class Light { public: void StartLight(); void StopLight(); }; class StreetLight : Light, Transform { public: void StartLight() { /* ... */ } }; class Player : Light, Transform { public: void StartLight() { /* ... */ } } 后来游戏逐渐更新,玩家的灯光逻辑变得越来越复杂… ...

January 20, 2026 · 11 min · SSmallOrange

Hazel学习笔记:Renderer2D(持续更新)

Hazel学习笔记:Renderer2D部分 ​ 最近一段时间一直想拓展一下个人的技术栈,想学习一些图形学的知识,毕竟目前在做客户端相关的开发,学一些图形相关的知识应该也没有什么坏处,从我过去一段时间的工作经历来看:只要是学过的芝士,总有一天会用得上的。 ​ 正好翻B站看到了有人搬运了TheCherno大佬的Hazel游戏引擎教程,就决定从这里入手学习,了解OpenGL和一些图形学的基础知识,并将一些芝士整理在此, 对于引擎基础部分的搭建就先略过(比如:日志部分(log)、项目搭建(Premake)、事件、窗口等,后面可以单开一节整理Premake和性能分析部分),重点整理他对OpenGL的封装部分,以及后面的Renderer2D的抽象。 ​ 整体架构 OpenGL封装 Hazel从OpenGL的“Hello World”入手,逐步减少原生OpenGL代码的编写,所以我们首先需要知道,原生OpenGL绘制一个三角形都需要哪些步骤? 首先,需要准备一些顶点数据: 1 2 3 4 5 6 float vertices[] = { // 位置 // 颜色 -0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.5f, 0.0f, 0.0f, 0.0f, 1.0f }; 准备VAO/VBO: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 unsigned int vao, vbo; glGenVertexArrays(1, &vao); glGenBuffers(1, &vbo); glBindVertexArray(vao); glBindBuffer(GL_ARRAY_BUFFER, vbo); glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); // 位置属性,location = 0 glEnableVertexAttribArray(0); glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0); // 颜色属性,location = 1 glEnableVertexAttribArray(1); glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(3 * sizeof(float))); glBindVertexArray(0); 准备着色器: ...

November 24, 2025 · 6 min · 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>; 在第一层类型检查通过后,我们就需要对具体类型进行递归分发了,首先我们要能够循环的遍历结构体成员: ...

November 15, 2025 · 4 min · 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)就没区别了,这时候上一章讲到的结构化绑定就派上用场了,能够想到: 1 2 3 4 5 int main () { static Person p; auto& [age, name] = p; get_member_type_name<&age>(); } 但是编译后会发现报错:error C2672: “get_member_type_name”: 未找到匹配的重载函数,这是因为age 是局部结构化绑定引用,&age 不是地址常量表达式。所以对于&p是可以的,但是&age不行。核心在于,我们需要告诉编译器,age是一个编译期常量,对于这个问题,解决方案如下: ...

August 12, 2025 · 4 min · 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 接收模板参数,这在编译期展开成员信息时非常方便。 1 2 3 4 5 auto print_indices = []<std::size_t... Is>(std::index_sequence<Is...>) { // C++20支持模板 ((std::cout << Is << " "), ...); // C++17 折叠表达式 }; print_indices(std::make_index_sequence<5>{}); // 输出:0 1 2 3 4 这里的折叠表达式能够将模板参数包进行展开,这种写法在后面会经常用到,是反射利器。 ...

August 11, 2025 · 3 min · SSmallOrange

技术碎片(持续更新)

Modern C++ Grammar std::ranges std::ranges 库的核心思想是让算法直接操作容器(而非迭代器对),并支持管道式(pipeline)链式调用。 对比以前的迭代器使用方式: 1 2 3 4 5 6 7 // 找到指定元素 std::sort(v.begin(), v.end()); auto it = std::find(v.begin(), v.end(), 4); // std::ranges std::ranges::sort(v); auto it = std::ranges::find(v, 4); 以下是一些std::ranges的常用方式: 转换: std::transform 过滤: std::filter 排序: std::sort 查找: std::find 遍历: for_each 1 2 3 // 将数组内所有元素置0 std::vector<int> matrix{0, 1, 0, 2, 3}; std::ranges::for_each(matrix, [](auto& value) { value = 0; }); 判断是否存在满足条件的元素: contains 1 2 3 // 检查是否包含0元素 std::vector<int> matrix{0, 1, 0, 2, 3}; bool hasZero = std::ranges::contains(matrix, 0); any_of 1 2 3 4 5 // 检查是否包含0元素 std::vector<int> matrix{0, 1, 0, 2, 3}; bool hasZero = std::ranges::any_of(matrix, [](auto& value) -> bool { return value == 0 }); 其他: ...

July 20, 2025 · 11 min · SSmallOrange