这是反射系列的最后一篇。前面几篇依次介绍了 ^^ 与 [: :]、<meta> 查询、编译期类型合成、动态访问,以及用反射实现的通用 formatter。
前面是把 C++ 对象中的字段读出来,今天我们反过来,把 JSON 中的字段映射到 C++ 结构体。这个例子会串起成员查询、template for 和成员拼接。
假设有这样一个类型:
struct User {
int id;
std::string name;
};
要从 JSON 中读取它,通常要手动写出 "id" 对应 user.id、"name" 对应 user.name。以后给 User 增加字段,解析代码也得跟着改。宏或代码生成器能减少重复劳动,而 C++26 静态反射给出了另一条路:在编译期查询成员,再为每个成员生成解析代码。
本文只实现 JSON → C++ 的反序列化。JSON 文本解析交给现有 JSON 库;这里的重点是,拿到 JSON 对象后,如何把它映射到结构体。
核心思路
JSON 的键是运行时字符串,C++ 的成员则在编译期确定。我们需要把两者接起来:
- 在编译期取得结构体的数据成员及其名称。
- 为每个成员生成一段代码。
- 在运行时检查 JSON 是否包含同名键。
- 如果存在,把 JSON 中的值赋给对应成员。
这里不会在运行时通过字符串“寻找 C++ 成员”。template for 会针对每个成员分别展开,成员访问也会在编译期确定。运行时需要做的只是查找 JSON 键、读取值。
Json 接口
为了把注意力放在反射上,先假设已有这样一个 JsonObject 接口:
struct JsonObject {
bool contains(std::string_view key) const;
template <typename T>
T get(std::string_view key) const;
JsonObject get_child(std::string_view key) const;
std::vector<JsonObject> get_array(std::string_view key) const;
};
contains 检查键是否存在,get<T> 读取基本类型,get_child 和 get_array 分别取得嵌套对象和对象数组。
基本类型
先写一个只处理 int、float、std::string 等字段的 from_json<T>。成员查询返回的是 vector;为了给 template for 使用,我们把反射信息复制进固定大小的 std::array:
#include <meta>
#include <array>
#include <cstddef>
template <typename T>
consteval auto get_member_reflections()
{
constexpr auto ctx = std::meta::access_context::current();
constexpr std::size_t count =
std::meta::nonstatic_data_members_of(^^T, ctx).size();
auto members =
std::meta::nonstatic_data_members_of(^^T, ctx);
std::array<std::meta::info, count> result{};
for (std::size_t i = 0; i < count; ++i) {
result[i] = members[i];
}
return result;
}
有了成员列表,反序列化函数可以这样写:
template <typename T>
T from_json(const JsonObject& json)
{
T obj{};
constexpr auto members = get_member_reflections<T>();
template for (constexpr auto member : members)
{
constexpr auto name =
std::meta::identifier_of(member);
using FieldType =
typename [: std::meta::type_of(member) :];
if (json.contains(name)) {
obj.[: member :] = json.get<FieldType>(name);
}
}
return obj;
}
关键在执行成员访问与赋值的那一行:
obj.[: member :] = json.get<FieldType>(name);
假如本次展开对应 User::id,那么 name 是 "id",FieldType 是 int,成员访问对应 obj.id。JSON 键是否存在仍在运行时判断,但访问哪个 C++ 成员,编译器已经确定。
复杂字段
基本版本还不能直接处理嵌套结构体、std::optional 和 std::vector。我们可以保留刚才的成员遍历,在循环体内用 if constexpr 根据字段类型选择解析方式。
先准备几个类型萃取:
#include <optional>
#include <type_traits>
#include <vector>
template <typename T>
struct is_optional : std::false_type {};
template <typename T>
struct is_optional<std::optional<T>> : std::true_type {};
template <typename T>
struct is_vector : std::false_type {};
template <typename T, typename Allocator>
struct is_vector<std::vector<T, Allocator>> : std::true_type {};
template <typename T>
struct inner_type;
template <typename T>
struct inner_type<std::optional<T>> {
using type = T;
};
template <typename T, typename Allocator>
struct inner_type<std::vector<T, Allocator>> {
using type = T;
};
对于 std::optional<int>,取出内部的 int 后即可读取 JSON 值;如果 JSON 缺少该键,就保留空值。对于 std::vector<Vector2>,则遍历 JSON 对象数组,对每个元素递归调用 from_json<Vector2>。
下面把这些分支放进同一个函数。
完整示例
这个例子用 std::map 和 std::any 模拟 JSON 对象,方便集中展示反射部分。它没有解析 JSON 字符串;实际项目需要由 JSON 库完成文本解析,并提供与这里相应的取值接口。
#include <meta>
#include <any>
#include <array>
#include <cstddef>
#include <iostream>
#include <map>
#include <optional>
#include <string>
#include <string_view>
#include <type_traits>
#include <vector>
struct JsonObject {
std::map<std::string, std::any> fields;
bool contains(std::string_view key) const
{
return fields.contains(std::string(key));
}
template <typename T>
T get(std::string_view key) const
{
return std::any_cast<T>(fields.at(std::string(key)));
}
JsonObject get_child(std::string_view key) const
{
return std::any_cast<JsonObject>(
fields.at(std::string(key))
);
}
std::vector<JsonObject> get_array(
std::string_view key
) const
{
return std::any_cast<std::vector<JsonObject>>(
fields.at(std::string(key))
);
}
};
template <typename T>
struct is_optional : std::false_type {};
template <typename T>
struct is_optional<std::optional<T>> : std::true_type {};
template <typename T>
struct is_vector : std::false_type {};
template <typename T, typename Allocator>
struct is_vector<std::vector<T, Allocator>>
: std::true_type {};
template <typename T>
struct inner_type;
template <typename T>
struct inner_type<std::optional<T>> {
using type = T;
};
template <typename T, typename Allocator>
struct inner_type<std::vector<T, Allocator>> {
using type = T;
};
template <typename T>
consteval auto get_member_reflections()
{
constexpr auto ctx = std::meta::access_context::current();
constexpr std::size_t count =
std::meta::nonstatic_data_members_of(^^T, ctx).size();
auto members =
std::meta::nonstatic_data_members_of(^^T, ctx);
std::array<std::meta::info, count> result{};
for (std::size_t i = 0; i < count; ++i) {
result[i] = members[i];
}
return result;
}
template <typename T>
T from_json(const JsonObject& json)
{
T obj{};
constexpr auto members = get_member_reflections<T>();
template for (constexpr auto member : members)
{
constexpr auto name =
std::meta::identifier_of(member);
using FieldType =
typename [: std::meta::type_of(member) :];
if (!json.contains(name)) {
continue;
}
if constexpr (is_optional<FieldType>::value)
{
using Inner =
typename inner_type<FieldType>::type;
obj.[: member :] = json.get<Inner>(name);
}
else if constexpr (is_vector<FieldType>::value) {
using Inner =
typename inner_type<FieldType>::type;
// 这个示例处理“嵌套结构体数组”
for (const auto& item : json.get_array(name)) {
obj.[: member :].push_back(
from_json<Inner>(item)
);
}
}
else if constexpr (
std::meta::is_class_type(^^FieldType) &&
!std::is_same_v<FieldType, std::string>
) {
obj.[: member :] =
from_json<FieldType>(json.get_child(name));
}
else {
obj.[: member :] =
json.get<FieldType>(name);
}
}
return obj;
}
struct Vector2 {
float x;
float y;
};
struct Enemy {
std::string name;
int health;
std::optional<int> shield;
std::vector<Vector2> patrol_path;
};
int main()
{
JsonObject first_point;
first_point.fields["x"] = 100.5f;
first_point.fields["y"] = 200.0f;
JsonObject second_point;
second_point.fields["x"] = 150.0f;
second_point.fields["y"] = 250.5f;
JsonObject payload;
payload.fields["name"] = std::string("General Radahn");
payload.fields["health"] = 5000;
payload.fields["shield"] = 150;
payload.fields["patrol_path"] =
std::vector<JsonObject>{
first_point, second_point
};
Enemy enemy = from_json<Enemy>(payload);
std::cout << "Name: " << enemy.name << '\n';
std::cout << "Health: " << enemy.health << '\n';
if (enemy.shield) {
std::cout << "Shield: " << *enemy.shield << '\n';
}
std::cout << "Waypoints: "
<< enemy.patrol_path.size() << '\n';
std::cout << "First waypoint: ("
<< enemy.patrol_path[0].x << ", "
<< enemy.patrol_path[0].y << ")\n";
}
在这个例子中,Enemy::shield 走 optional 分支,Enemy::patrol_path 走数组分支,而数组里的每个 Vector2 再由同一个 from_json 递归解析。结构体自身不需要继承某个基类,也不需要添加注册宏。
访问控制与错误处理
这里使用的是 std::meta::access_context::current(),所以成员查询遵循当前上下文的访问权限。示例中的字段都是公有成员。如果使用 unchecked() 查询私有成员,还需要认真考虑哪些数据允许被反序列化,以及是否应该暴露这些字段。
缺失字段目前会保留 T{} 初始化后的值。这适合演示,但实际项目往往需要区分“允许缺失的字段”和“必须存在的字段”。此外,std::any_cast 在类型不匹配时会抛出异常;真实 JSON 库还需要处理数字范围、null、数组元素错误等情况。
这个示例的 vector 分支只处理嵌套结构体数组,例如 std::vector<Vector2>。如果新增 std::vector<std::string>,还需要增加读取基础类型数组的分支,不能直接套用当前的递归调用。
结语
这个例子最值得关注的地方,是 template for 与成员拼接的配合:编译器在编译期逐个展开结构体字段,而程序在运行时根据 JSON 键读取对应的值。新增一个当前解析逻辑支持的字段时,就不用再手动维护一份同名字段映射。
说白了,反射解决的是认识并访问 C++ 类型结构的问题;JSON 文本解析、类型转换和错误处理仍要由完整的序列化库承担。把这两部分分清楚,才能看清静态反射真正省掉了哪些重复代码。
以上