C++ 无反射 JSON 序列化:AIGCJson 的宏与模板魔法
C++ 缺少原生反射,要在运行时拿到成员变量的名称和类型几乎不可能,因此实现 JSON 序列化免不了大量样板代码。AIGCJson 的做法很取巧:靠预处理器宏和模板元编程,在编译期生成所有元数据和转换函数,让用户只写一行宏就能注册字段。
下面拆开看看它是怎么做到的。
怎么做到的?三步
AIGCJson 的套路可以总结成三步:
- 宏:利用
#__VA_ARGS__把成员变量列表原样变成字符串字面量,从而在运行时拿到字段名。 - 变长参数模板:把宏后面的参数包展开,在编译期逐一绑定到字段引用上。
- SFINAE:编译期判断类型,对基础类型、容器和自定义结构体走不同的处理分支。
AIGC_JSON_HELPER 宏
整个库的入口就是这个宏:
#define AIGC_JSON_HELPER(...) \
std::map<std::string, std::string> __aigcDefaultValues; \
bool AIGCJsonToObject(aigc::JsonHelperPrivate &handle, rapidjson::Value &jsonValue, std::vector<std::string>&names) { \
std::vector<std::string> standardNames = handle.GetMembersNames(#__VA_ARGS__); \
if (names.size() <= standardNames.size()) { \
for (int i = names.size(); i < (int)standardNames.size(); i++) { \
names.push_back(standardNames[i]); \
} \
} \
return handle.SetMembers(names, 0, jsonValue, __aigcDefaultValues, __VA_ARGS__); \
}
在类里写 AIGC_JSON_HELPER(name, age) 时,编译器会在该类中插入 AIGCJsonToObject 和 AIGCObjectToJson 两个成员函数,一个负责反序列化,一个负责序列化。
#__VA_ARGS__ 是预处理器字符串化操作符,它会把 name, age 直接变成字符串 "name, age",也就是说,字段名不是通过反射推断出来的,就是把源代码里的文本直接拿过去了。
字段名提取
拿到 "name, age" 这个字符串之后,JsonHelperPrivate::GetMembersNames 做的工作很简单:按逗号分割,去掉空格和引号,返回一个 vector<string> {"name", "age"},这就是 JSON 的 Key 了。
std::vector<std::string> GetMembersNames(const std::string membersStr) {
std::vector<std::string> array = StringSplit(membersStr);
StringTrim(array);
return array;
}
这一步发生在运行期,没什么魔幻的。
变长参数模板递归:把名字和值绑到一起
有了 Key 字符串数组,还需要拿到对应的 Value,也就是成员变量的引用。AIGCJsonToObject 里调用 handle.SetMembers 时,传入了 __VA_ARGS__(即 name, age 这些变量)。这个函数使用变长参数模板,递归处理参数列表。
// 终止递归:处理最后一个参数
template<typename TYPE>
bool SetMembers(const std::vector<std::string>&names, int index, rapidjson::Value &jsonValue, ..., TYPE &arg) {
const char* key = names[index].c_str();
if (!jsonValue.HasMember(key)) {
/* 处理默认值或返回 */
}
if (!JsonToObject(arg, jsonValue[key])) return false;
return true;
}
// 递归展开函数
template<typename TYPE, typename... TYPES>
bool SetMembers(const std::vector<std::string>&names, int index, rapidjson::Value &jsonValue, ..., TYPE &arg, TYPES &...args) {
if (!SetMembers(names, index, jsonValue, ..., arg)) return false;
return SetMembers(++index, jsonValue, ..., args...);
}
假设注册了 name, age,调用流程是这样的:
- 进入展开函数,
TYPE是string,arg是name。它调用单参数版本的SetMembers,用root["name"]解析并赋值给name。 - 然后递归调用自身,
index加 1,参数包剩下age。 - 再次进入展开函数,
TYPE是int,arg是age,同样调用单参数版本解析root["age"]。 - 参数包为 0,递归结束。
就这样,运行期的 names 向量和编译期的参数包被按顺序一一对应起来了。这个递归展开是编译期完成的,所以没有运行时开销。
SFINAE 类型分发
SetMembers 内部解析值的时候,调用的是 JsonToObject(arg, jsonValue)。这个函数需要处理各种类型:int、string、vector、自定义结构体…… 库依赖 SFINAE 来判断类型并分发到不同实现。
首先用 HasConvertFunction 检查一个类型有没有 AIGCJsonToObject 成员函数:
template<typename T>
struct HasConvertFunction {
template<typename TT>
static char func(decltype(&TT::AIGCJsonToObject));
template<typename TT>
static int func(...);
const static bool has = (sizeof(func<T>(nullptr)) == sizeof(char));
};
如果你的类用了 AIGC_JSON_HELPER 宏,它就包含 AIGCJsonToObject,has 就是 true。然后配合 enable_if 实现路由:
// 自定义类型
template<typename T, typename enable_if<HasConvertFunction<T>::has, int>::type = 0>
bool JsonToObject(T &obj, rapidjson::Value &jsonValue) {
return obj.AIGCJsonToObject(*this, jsonValue, names);
}
// 基础类型(无 AIGCJsonToObject)
template<typename T, typename enable_if<!HasConvertFunction<T>::has, int>::type = 0>
bool JsonToObject(T &obj, rapidjson::Value &jsonValue) {
if (std::is_enum<T>::value) { ... }
return false; // 不支持的会报错
}
至于 int、string 这些基础类型,库里直接提供了全特化的重载:
bool JsonToObject(int& obj, rapidjson::Value &jsonValue);
bool JsonToObject(std::string &obj, rapidjson::Value &jsonValue);
// ...
容器与嵌套的自动处理
std::vector、std::map 之类的容器通过模板重载支持。以 vector 为例:
template<typename TYPE>
bool JsonToObject(std::vector<TYPE>&obj, rapidjson::Value &jsonValue) {
if (!jsonValue.IsArray()) return false;
auto array = jsonValue.GetArray();
for (int i = 0; i < array.Size(); i++) {
TYPE item;
if (!JsonToObject(item, array[i])) return false;
obj.push_back(item);
}
return true;
}
这里 TYPE 既可以是基础类型,也可以是自定义结构体。因为 JsonToObject(item, array[i]) 会再次进入 SFINAE 路由,碰上结构体就调用它的 AIGCJsonToObject。所以 vector<User> 也能正常工作。
嵌套结构就更自然了。解析 User.address 时:
SetMembers调用JsonToObject(address, jsonValue["address"])。Address类型有AIGC_JSON_HELPER,SFINAE 把它送到自定义类型分支。- 调用
address.AIGCJsonToObject(...),进入Address内部的整套逻辑。
整个过程一环套一环,不需要额外处理。
几个实用的高级特性
字段重命名:使用 AIGC_JSON_HELPER_RENAME 宏可以定义重命名函数,解析阶段的 JsonToObject 会优先检查是否存在这个函数,如果有就用它返回的 names 列表替换默认的字段名。AIGCJsonToObject 里还会做一次补齐:如果重命名列表长度不够,会用原名补足。
默认值:AIGC_JSON_HELPER_DEFAULT 宏生成了 AIGCDefaultValues,解析类似 "age=18" 的字符串并存入 __aigcDefaultValues 映射。解析 JSON 字段时,如果某个 Key 不存在,就到这个映射里找默认值,用 StringToObject 转换成对应类型赋给变量。
继承:AIGC_JSON_HELPER_BASE 宏生成的代码会调用 SetBase,这个函数只是简单转发调用基类的 JsonToObject,复用整套序列化逻辑。
这些机制凑在一起,让 AIGCJson 在不侵入类继承体系的情况下,把 RapidJSON 的 DOM 接口和 C++ 对象模型粘合得很干净。没有复杂的 DSL,没有代码生成步骤,只靠预处理和模板就做到了。理解了这套东西,再遇到类似需求时,自己动手扩展一下(比如加个新容器类型或换一种序列化格式)就心里有底了。

