Glaze 序列化 std::pair 踩坑全解:解决 expected_quote expected_bracket 与模板二义冲突

Glaze 序列化 std::pair 踩坑全解:解决 expected_quote / expected_bracket 与模板二义冲突

前言

最近基于 Glaze 做 JSON 序列化时,使用 std::pair 遇到两个致命问题:

  1. 序列化输出非法 JSON
    Glaze 原生对 std::pair<T1,T2> 的默认规则是 {pair.first值: pair.second值}
    如果 first 是数组/复杂结构体,会直接生成 {"[{...}]": [...]},JSON 规范要求对象键只能是普通字符串,解析时直接抛出 expected_quote / expected_bracket 语法错误。
  2. 自定义特化引发模板二义性编译报错
    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 隔离层 + 自动化宏

核心思路:

  1. 新建包装类 PairWrapper<T1,T2>,成员命名 _a/_b禁用 first/second ,不会触发 pair_t 匹配,消除模板冲突;
  2. 通过 glz::meta 映射 JSON 键 "first"/"second" 到内部 _a/_b,输出标准对象结构;
  3. 提供宏 DECLARE_GLZ_PAIR_WRAPPER(PairAlias),仅需一行代码为任意 std::pair 别名自动生成 to/from 特化;
  4. 业务代码完全保留 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

  1. 非法JSON键问题
    中转 PairWrapper 强制输出 {"first":..., "second":...} 对象结构,不会将数组/结构体作为JSON键 如 "[]{}": ,消除 expected_quote / expected_bracket 解析错误。
  2. 模板二义编译报错
    PairWrapper 内部字段命名 _a/_b,不匹配 Glaze pair_t concept,内置 pair 特化不会参与重载决议,无模板冲突,编译零报错。

2. 业务零侵入,改造成本极低

  • 业务变量仍使用 std::pair,接口、函数、数据结构无需全局修改;
  • 新增任意 std::pair 别名仅需一行宏 DECLARE_GLZ_PAIR_WRAPPER(别名)

3. 严格JSON校验配套

封装 strict_read_json,自动开启缺失键校验,适合后端接口入参校验,提前拦截不完整请求。

4. 兼容所有新版 Glaze

不依赖内部私有API(如废弃 write_key),使用官方标准 glz::metaglz::object 反射接口,升级 Glaze 版本无兼容性风险。

六、使用注意事项

  1. 宏调用顺序要求
    必须先 typedef std::pair<T1,T2> TestPair; 定义别名,再执行 DECLARE_GLZ_PAIR_WRAPPER(TestPair)
  2. 禁止给 PairWrapper 添加 first/second 成员
    一旦命名为 first/second,会被 pair_t 匹配,重新触发模板冲突;
  3. 仅适用于 JSON 序列化
    该方案基于 glz::to<JSON, T> 特化,beve 二进制序列化不影响业务;
  4. 多重pair嵌套支持
    宏自动推导 first_type/second_type,支持 std::pair<std::vector<std::pair<...>>, ...> 多层嵌套场景。

七、替代方案对比

方案 优点 缺点
直接替换为自定义结构体 逻辑最简单,无模板复杂度 业务代码大规模改造,侵入强
原生手写std::pair to/from特化 不改业务类型 模板二义编译报错,无法使用
PairWrapper+宏自动化(本文方案) 零业务侵入、一键注册、无编译冲突、标准JSON输出 少量模板封装学习成本

八、结尾

在 C++ 高性能 JSON 库 Glaze 的实际项目开发中,std::pair 序列化是高频踩坑点,原生设计不符合后端接口通用 JSON 对象规范。本文提供的隔离包装层+自动化宏方案,兼顾兼容性、可维护性与性能,彻底解决语法解析错误与模板编译冲突,可直接复制头文件落地项目。