如何通过C++模板实现JSON编解码器(codec)类型以简化遗留JSON解析器的使用
如何通过C++模板实现JSON编解码器(codec)类型以简化遗留JSON解析器的使用
设计合理性分析
你的这个设计思路非常合理,甚至可以说是针对这类重复编解码逻辑的最优解之一,原因如下:
- 消除样板代码:原有的
encode/decode函数中重复的put/get调用被模板化封装,只需要一次定义类型与键的对应关系,避免了冗余代码。 - 提升可维护性:修改成员、键名或类型时,只需修改编解码器的定义,无需在两个函数中分别同步修改,大幅降低了漏改、错改的风险。
- 编译期安全检查:类型与键的绑定关系在编译期固定,静态断言可以提前发现类型/键数量不匹配的问题,避免运行时因拼写错误或类型不匹配导致的bug。
实现方案
根据你允许使用最新C标准的要求,以下提供几种适配不同场景的实现,其中C20版本最简洁,char键版本完全贴合你的伪代码思路。
方案1:C++20 简洁版(推荐)
利用C++20支持的std::string_view非类型模板参数,直接将JSON键作为字符串字面量传入模板,代码直观且扩展性强:
#include <tuple> #include <string_view> #include <utility> #include <string> // 假设遗留JSON API与你提供的一致 namespace json { struct json_value; json_value* new_object(size_t size); void put(json_value* obj, const char* key, bool value); void put(json_value* obj, const char* key, int value); void put(json_value* obj, const char* key, float value); void put(json_value* obj, const char* key, const std::string& value); void get(const json_value* obj, const char* key, bool& value); void get(const json_value* obj, const char* key, int& value); void get(const json_value* obj, const char* key, float& value); void get(const json_value* obj, const char* key, std::string& value); } template <typename... Ts, std::string_view... Keys> struct JsonCodec { // 编译期检查:类型数量必须与键数量一致 static_assert(sizeof...(Ts) == sizeof...(Keys), "类型数量必须与JSON键数量匹配"); // 编码:接收对应类型的参数,生成JSON对象 static json::json_value* encode(const Ts&... values) { constexpr size_t member_count = sizeof...(Ts); auto* obj = json::new_object(member_count); // 用索引序列遍历键与值的对应关系,展开put调用 auto put_impl = [obj]<size_t... Is>(std::index_sequence<Is...>, const auto&... vs) { const auto* keys[] = {Keys.data()...}; const auto values_tuple = std::make_tuple(vs...); (json::put(obj, keys[Is], std::get<Is>(values_tuple)), ...); }; put_impl(std::index_sequence_for<Ts...>{}, values...); return obj; } // 解码方式1:返回tuple,配合C++17+结构化绑定使用 static std::tuple<Ts...> decode(const json::json_value* obj) { std::tuple<Ts...> result; const auto* keys[] = {Keys.data()...}; auto get_impl = [obj, keys]<size_t... Is>(std::index_sequence<Is...>) { (json::get(obj, keys[Is], std::get<Is>(result)), ...); }; get_impl(std::index_sequence_for<Ts...>{}); return result; } // 解码方式2:直接赋值给变量,与原代码用法更贴近 template <typename... Vs> static void decode(const json::json_value* obj, Vs&... vars) { static_assert(sizeof...(Vs) == sizeof...(Ts), "变量数量必须与编解码器类型数量匹配"); const auto* keys[] = {Keys.data()...}; auto vars_tuple = std::tie(vars...); auto get_impl = [obj, keys]<size_t... Is>(std::index_sequence<Is...>) { (json::get(obj, keys[Is], std::get<Is>(vars_tuple)), ...); }; get_impl(std::index_sequence_for<Ts...>{}); } };
方案2:贴近你伪代码的char键版本(C++17+)
如果你的JSON键确实是单个字符(如'b'、'i'),可以直接用char作为非类型模板参数,完全贴合你的伪代码思路:
template <typename... Ts, char... Keys> struct JsonCodec { static_assert(sizeof...(Ts) == sizeof...(Keys), "类型数量必须与键数量匹配"); static json::json_value* encode(const Ts&... values) { constexpr size_t member_count = sizeof...(Ts); auto* obj = json::new_object(member_count); // 转换单个char为C风格字符串(自动补'\0') const char* keys[] = {{Keys, '\0'}...}; auto put_impl = [obj, keys]<size_t... Is>(std::index_sequence<Is...>, const auto&... vs) { auto values_tuple = std::make_tuple(vs...); (json::put(obj, keys[Is], std::get<Is>(values_tuple)), ...); }; put_impl(std::index_sequence_for<Ts...>{}, values...); return obj; } // decode函数实现与方案1类似,仅键的生成方式不同 static std::tuple<Ts...> decode(const json::json_value* obj) { std::tuple<Ts...> result; const char* keys[] = {{Keys, '\0'}...}; auto get_impl = [obj, keys]<size_t... Is>(std::index_sequence<Is...>) { (json::get(obj, keys[Is], std::get<Is>(result)), ...); }; get_impl(std::index_sequence_for<Ts...>{}); return result; } };
使用示例
方案1(C++20)的使用:
// 1. 定义编解码器:绑定类型与对应的JSON键 using FooCodec = JsonCodec<bool, int, float, std::string, "b", "i", "f", "s">; // 2. 编码示例 bool b_val = true; int i_val = 42; float f_val = 3.14f; std::string s_val = "hello"; json::json_value* encoded_obj = FooCodec::encode(b_val, i_val, f_val, s_val); // 3. 解码示例:结构化绑定(C++17+) auto [decoded_b, decoded_i, decoded_f, decoded_s] = FooCodec::decode(encoded_obj); // 4. 解码示例:直接赋值给变量(兼容原有习惯) bool new_b; int new_i; float new_f; std::string new_s; FooCodec::decode(encoded_obj, new_b, new_i, new_f, new_s);
方案2(char键版本)的使用:
// 完全贴合你伪代码的编解码器定义 using FooCodec = JsonCodec<bool, int, float, std::string, 'b', 'i', 'f', 's'>; // 编码/解码用法与方案1完全一致 json::json_value* encoded_obj = FooCodec::encode(b_val, i_val, f_val, s_val); auto [decoded_b, decoded_i, decoded_f, decoded_s] = FooCodec::decode(encoded_obj);
关键设计要点
- 可变模板参数:用模板参数包
Ts...和Keys...接收类型与键的对应序列,保证扩展性。 - 索引序列与折叠表达式:利用
std::index_sequence(C11+,C11需自行实现)和折叠表达式(C++17+)遍历参数包,实现批量put/get操作,避免手动展开。 - 编译期断言:通过
static_assert提前检查类型/键数量匹配问题,将错误暴露在编译阶段。 - 灵活性兼容:提供两种解码方式,既支持现代C++的结构化绑定,也兼容原有直接赋值的习惯,降低迁移成本。
内容来源于stack exchange
相关产品推荐
相关产品推荐

