Glaze 序列化 std::pair 踩坑全解:解决 expected_quote / expected_bracket 与模板二义冲突
前言
最近基于 Glaze 做 JSON 序列化时,使用 std::pair 遇到两个致命问题:
- 序列化输出非法 JSON
Glaze 原生对std::pair<T1,T2>的默认规则是{pair.first值: pair.second值}。
如果first是数组/复杂结构体,会直接生成{"[{...}]": [...]},JSON 规范要求对象键只能是普通字符串,解析时直接抛出expected_quote/expected_bracket语法错误。 - 自定义特化引发模板二义性编译报错
Glaze 内置pair_t<T>concept 全局匹配所有std::pair,手写glz::to/glz::from特化后编译器存在两套候选模板,报ambiguous template instantiation,连锁触发不完整类型报错。
业务需求:JSON 输出标准结构
json
"testPair": {
"first": [...first数组...],
"second": [...second数组...]
}
Glaze输出标准std::pair的JSON结构
json
"testPair": {
"[...first数组...]": [...second数组...]
}
不想修改业务变量类型(大量代码依赖 std::pair),因此设计一套无侵入、宏自动化适配方案:PairWrapper 隔离原生 pair 逻辑冲突。
一、问题根源深度分析
1. Glaze 原生 std::pair 序列化逻辑
Glaze 源码内置特化:匹配所有满足 pair_t 的类型(std::pair),序列化规则:
std::pair<A,B>→{A: B}
当 A 为vector<结构体>,序列化后键是带"[]{}":的复杂字符串,违反 JSON 语法,反解析直接崩溃。
2. 自定义 to/from 特化的编译冲突
若直接对 std::pair 手写 glz::to<JSON, std::pair<...>>:
- 候选1:Glaze 内置
template<T> requires pair_t<T> struct to - 候选2:我们自定义的
template<> struct to<JSON, std::pair<...>>
编译器无法选择,报模板歧义错误,项目无法编译。
3. 为什么结构体替代方案有局限
直接用 struct TestPair { T1 first; T2 second; } 能规避序列化问题,但项目中大量接口、变量、函数返回值强依赖 std::pair,全局修改成本极高,侵入业务代码。
二、解决方案:PairWrapper 隔离层 + 自动化宏
核心思路:
- 新建包装类
PairWrapper<T1,T2>,成员命名_a/_b,禁用 first/second ,不会触发pair_t匹配,消除模板冲突; - 通过
glz::meta映射 JSON 键"first"/"second"到内部_a/_b,输出标准对象结构; - 提供宏
DECLARE_GLZ_PAIR_WRAPPER(PairAlias),仅需一行代码为任意std::pair别名自动生成to/from特化; - 业务代码完全保留
std::pair写法,零业务改造。
完整头文件 PairWrapper.h
cpp
#ifndef PAIRWRAPPER_H
#define PAIRWRAPPER_H
#include <glaze/glaze.hpp>
#include <utility>
#include <type_traits>
namespace glz {
/**
* @brief 通用 pair 包装模板,规避 glaze 原生 std::pair 序列化逻辑冲突
* @tparam T1 pair.first 对应类型
* @tparam T2 pair.second 对应类型
* 关键点:成员命名 _a/_b,不使用 first/second,不会被 pair_t concept 匹配,消除模板二义性
*/
template <typename T1, typename T2>
struct PairWrapper {
// 对外暴露和 std::pair 一致的类型别名,方便上层转换
using first_type = T1;
using second_type = T2;
// 实际存储字段,规避 glaze 内置 pair 识别
T1 _a;
T2 _b;
// 默认构造
PairWrapper() = default;
// 从两个值构造,模拟 pair 构造逻辑
PairWrapper(T1 f, T2 s) : _a(std::move(f)), _b(std::move(s)) {}
// CTAD 类模板参数推导,支持 PairWrapper(val1, val2) 自动推导类型
template <typename U1, typename U2>
PairWrapper(U1&& u1, U2&& u2)
: _a(std::forward<U1>(u1)), _b(std::forward<U2>(u2))
{}
};
/**
* @brief CTAD 推导指引,简化 PairWrapper 构造时类型书写
* 输入两个值自动推导为 PairWrapper<衰减后的类型1, 衰减后的类型2>
*/
template <typename U1, typename U2>
PairWrapper(U1&&, U2&&) -> PairWrapper<std::decay_t<U1>, std::decay_t<U2>>;
/**
* @brief 为 PairWrapper 提供 glaze 反射元数据
* 序列化输出 JSON {"first": xxx, "second": xxx}
* 将 JSON 键 "first" 绑定内部成员 _a,"second" 绑定内部成员 _b
* @tparam T1 包装类第一元类型
* @tparam T2 包装类第二元类型
*/
template <typename T1, typename T2>
struct meta<PairWrapper<T1, T2>> {
static constexpr auto value = object(
"first", &PairWrapper<T1, T2>::_a,
"second", &PairWrapper<T1, T2>::_b
);
};
} // namespace glz
/**
* @brief 宏:使用声明 std::pair 别名,自动生成 glaze to/from 特化
* @param PairAlias 自定义pair别名(如 TestPair)
* 使用示例:DECLARE_GLZ_PAIR_WRAPPER(TestPair)
* 效果:同时生成 glz::to/glz::from 适配
*/
#define DECLARE_GLZ_PAIR_WRAPPER(PairAlias) \
namespace glz { \
/* 特化 glz::to:序列化 PairAlias(std::pair) 转发到 PairWrapper */ \
template <> \
struct to<JSON, PairAlias> { \
/* Opts 序列化配置参数,自动透传 */ \
template <auto Opts> \
static inline error_ctx op( \
const PairAlias& value, /* 原始std::pair对象 */ \
context& ctx, /* glaze上下文 */ \
std::string& buffer, /* 输出json字符串缓冲区 */ \
std::size_t& ix /* 缓冲区写入偏移索引 */ \
) { \
/* 对应当前pair的包装类型 */ \
using WrapperT = PairWrapper<typename PairAlias::first_type, \
typename PairAlias::second_type>; \
/* 将std::pair数据中转到包装类 */ \
WrapperT tmp{value.first, value.second}; \
/* 调用包装类的序列化逻辑,输出{"first":..., "second":...} */ \
to<JSON, WrapperT>::template op<Opts>(tmp, ctx, buffer, ix); \
/* 无错误返回空error */ \
return {}; \
} \
}; \
\
/* 特化 glz::from:反序列化JSON到 PairAlias(std::pair) */ \
template <> \
struct from<JSON, PairAlias> { \
template <auto Opts> \
static inline void op( \
PairAlias& value, /* 输出std::pair对象 */ \
context& ctx, /* glaze解析上下文 */ \
const char*& it, /* 当前解析指针 */ \
const char* end /* json字符串末尾指针 */ \
) { \
using WrapperT = PairWrapper<typename PairAlias::first_type, \
typename PairAlias::second_type>; \
WrapperT tmp; \
/* 先解析JSON到中间包装类 */ \
from<JSON, WrapperT>::template op<Opts>(tmp, ctx, it, end); \
/* 解析出错直接提前返回,不赋值 */ \
if (ctx.error != error_code::none) { \
return; \
} \
/* 将包装类解析结果移动赋值回原始std::pair */ \
value.first = std::move(tmp._a); \
value.second = std::move(tmp._b); \
} \
}; \
} // namespace glz
#endif //PAIRWRAPPER_H
三、配套工具:严格模式JSON读取函数
业务接口通常需要严格校验JSON字段,缺失任何结构体定义的键直接返回错误,避免脏数据,封装通用工具函数:
cpp
#pragma once
#include <glaze/glaze.hpp>
#include <optional>
/**
* @brief 严格模式读取JSON(引用参数版本)
* @details 严格读取JSON,要求目标结构体中定义的所有键都必须存在,缺失任何键都会导致解析失败。
* 拒绝任何不完整字段的JSON请求体,防止不完整数据提交。
* @tparam T 目标类型,必须支持glaze的JSON读取
* @tparam Buffer 输入缓冲区类型
* @param value 解析结果写入引用参数
* @param buffer 包含JSON数据的输入缓冲区
* @return glz::error_ctx 错误上下文,无错则为空
*/
template <glz::read_supported<glz::JSON> T, glz::is_buffer Buffer>
[[nodiscard]] inline glz::error_ctx strict_read_json(T &value,
Buffer &&buffer) {
glz::context ctx{};
// 开启缺失键报错:error_on_missing_keys = true
return read<glz::opts{.error_on_missing_keys = true}>(
value, std::forward<Buffer>(buffer), ctx);
}
四、完整单元测试示例(TestStrictReadJSON)
1. 定义业务结构体与 std::pair 别名
cpp
// 子结构体1
struct Test2 {
std::string name;
int age;
};
// 子结构体2,嵌套Test2数组
struct Test3 {
std::string name;
int age;
std::vector<Test2> test2s;
};
// 业务使用的std::pair别名,原有代码无需修改类型
typedef std::pair<std::vector<Test2>, std::vector<Test3>> TestPair;
// 外层请求体,包含pair字段
struct XXXRequest {
TestPair testPair;
};
// 引入PairWrapper头文件,注册序列化适配
#include "PairWrapper.h"
DECLARE_GLZ_PAIR_WRAPPER(TestPair)
2. 序列化&反序列化完整测试类
cpp
#include <iostream>
#include <string>
// #include <magic_enum/magic_enum.hpp> // 可选择
class TestStrictReadJSON {
public:
TestStrictReadJSON()
{
std::cout << "===== Start Glaze Pair Serialize Test =====" << std::endl;
XXXRequest request;
// 填充测试数据
request.testPair.first = { { "name1", 1 }, { "name2", 2 } };
request.testPair.second = { { "name2", 2, { {"name3",3}, {"name4",4} } } };
// 序列化输出JSON
std::string json = glz::write_json(request).value_or("");
std::cout << "序列化输出JSON:" << json << std::endl;
// 输出结果:{"testPair":{"first":[{"name":"name1","age":1},{"name":"name2","age":2}],"second":[{"name":"name2","age":2,"test2s":[{"name":"name3","age":3},{"name":"name4","age":4}]}]}}
// 不再出现非法键格式,完全符合预期
// 标准输入JSON字符串
const std::string in_json = R"({
"testPair": {
"first": [
{ "name": "name1", "age": 1 },
{ "name": "name2", "age": 2 }
],
"second": [
{
"name": "name2",
"age": 2,
"test2s": [
{ "name": "name3", "age": 3 },
{ "name": "name4", "age": 4 },
{ "name": "name5", "age": 5 }
]
}
]
}
})";
// 反序列化测试
try {
XXXRequest request2;
glz::error_ctx error = strict_read_json(request2, in_json);
if (error) {
// 打印错误码与信息
std::cout << "解析失败 | 错误码:"
#ifdef MAGIC_ENUM_VERSION_MAJOR
<< magic_enum::enum_name(error.ec)
#else
<< uint32_t(error.ec)
#endif
<< " | 错误详情:" << error.custom_error_message << std::endl;
} else {
std::cout << "解析成功,二次序列化验证:"
<< glz::write_json(request2).value_or("") << std::endl;
}
} catch (std::exception &e) {
std::cout << "捕获异常: " << e.what() << std::endl;
}
std::cout << "===== End Glaze Pair Serialize Test =====" << std::endl;
};
~TestStrictReadJSON() = default;
};
五、核心方案优势总结
1. 彻底解决两大核心BUG
- 非法JSON键问题
中转PairWrapper强制输出{"first":..., "second":...}对象结构,不会将数组/结构体作为JSON键 如"[]{}":,消除expected_quote/expected_bracket解析错误。 - 模板二义编译报错
PairWrapper内部字段命名_a/_b,不匹配 Glazepair_tconcept,内置 pair 特化不会参与重载决议,无模板冲突,编译零报错。
2. 业务零侵入,改造成本极低
- 业务变量仍使用
std::pair,接口、函数、数据结构无需全局修改; - 新增任意
std::pair别名仅需一行宏DECLARE_GLZ_PAIR_WRAPPER(别名);
3. 严格JSON校验配套
封装 strict_read_json,自动开启缺失键校验,适合后端接口入参校验,提前拦截不完整请求。
4. 兼容所有新版 Glaze
不依赖内部私有API(如废弃 write_key),使用官方标准 glz::meta、glz::object 反射接口,升级 Glaze 版本无兼容性风险。
六、使用注意事项
- 宏调用顺序要求
必须先typedef std::pair<T1,T2> TestPair;定义别名,再执行DECLARE_GLZ_PAIR_WRAPPER(TestPair); - 禁止给 PairWrapper 添加 first/second 成员
一旦命名为 first/second,会被pair_t匹配,重新触发模板冲突; - 仅适用于 JSON 序列化
该方案基于glz::to<JSON, T>特化,beve 二进制序列化不影响业务; - 多重pair嵌套支持
宏自动推导first_type/second_type,支持std::pair<std::vector<std::pair<...>>, ...>多层嵌套场景。
七、替代方案对比
| 方案 | 优点 | 缺点 |
|---|---|---|
| 直接替换为自定义结构体 | 逻辑最简单,无模板复杂度 | 业务代码大规模改造,侵入强 |
| 原生手写std::pair to/from特化 | 不改业务类型 | 模板二义编译报错,无法使用 |
| PairWrapper+宏自动化(本文方案) | 零业务侵入、一键注册、无编译冲突、标准JSON输出 | 少量模板封装学习成本 |
八、结尾
在 C++ 高性能 JSON 库 Glaze 的实际项目开发中,std::pair 序列化是高频踩坑点,原生设计不符合后端接口通用 JSON 对象规范。本文提供的隔离包装层+自动化宏方案,兼顾兼容性、可维护性与性能,彻底解决语法解析错误与模板编译冲突,可直接复制头文件落地项目。