CXYVIP官网源码交易平台_网站源码_商城源码_小程序源码平台-丞旭猿论坛
CXYVIP官网源码交易平台_网站源码_商城源码_小程序源码平台-丞旭猿论坛
CXYVIP官网源码交易平台_网站源码_商城源码_小程序源码平台-丞旭猿论坛

简单的C++结构体字段反射-免费源码丞旭猿

微信限制:不能放置链接排版和样式比较奇怪;发布后不能更新。。。

推荐阅读原文

本文不讨论完整的 C++ 反射技术,只讨论结构体(struct)字段(field)反射,及其在序列化/反序列化代码生成上的应用。

正文开始于§ 静态反射部分,其他部分都是铺垫。。可以略读。。。😑

背景(TL;DR)

很多人喜欢把程序员称为码农,程序员也经常嘲讽自己每天都在搬砖。这时候,大家会想:能否构造出一些更好的工具,代替我们做那些无意义的体力劳动呢?

在实际 C++ 项目中,我们经常需要实现一些与外部系统交互的接口—— 外部系统传入 JSON 参数,我们的程序处理后,再以 JSON 的格式传回外部系统。这个过程就涉及到了两次数据结构的转换:

  • 输入的 JSON 转换为 C++ 数据结构(反序列化deserialization

  • C++ 数据结构 转换为 输出的 JSON(序列化serialization

如果传输的 JSON 数据格式(schema)非常繁多、比较复杂,那么序列化/反序列化的代码也会变得非常复杂 —— 需要处理结构嵌套可选字段输入合法性检查等问题。如果为每个 JSON 数据结构都人工手写一套序列化/反序列化代码,那么工作量会特别大。

例如,chromium/headless 的 devtools 相关接口 里就定义了 33 个领域模型(domain model),每个模型有自己的格式,其中又包含了许多字段。

懒惰是程序员的天性:

  • 勤奋 的程序员选择§ 人工手写 序列化/反序列化 代码

  • 懒惰 的程序员选择

    • 构建代码生成器(例如 protobuf、chromium/mojo)

    • § 编译器生成 序列化/反序列化 代码

代码生成器虽然功能强大,但依赖复杂,不易于和已有系统集成。所以本文主要讨论如何用 C++ 14 提供的元编程(metaprogramming)技巧,让编译器帮你写代码。🙄

目标(TL;DR)

  • 基于 C++原生语法,不需要引入第三方库

  • 提供声明式(declarative)的方法,只需要声明格式,不需要写逻辑语句

  • 不会带来额外的运行时开销,能达到和手写代码一样的运行时效率

基于 nlohmann 的 C++ JSON 库,给定两个 C++ 结构体SimpleStructNestedStruct

structSimpleStruct{boolbool_;intint_;doubledouble_;std::stringstring_;std::unique_ptr<bool> optional_;
};structNestedStruct{SimpleStruct nested_;std::vector vector_;
};
  • NestedStruct::nested_为嵌套对象,NestedStruct::vector_为嵌套的对象数组

  • SimpleStruct::optional_为可选字段;由于std::optional需要 C++ 17 支持,所以我们使用std::unique_ptr表示可选字段

  • 针对可选字段的 JSON 序列化/反序列化扩展代码,见optional_json.h(参考:How do I convert third-party types? | nlohmann/json)

一般接口的业务处理,往往包括三部分:

  • 解析输入(字符串到 JSON 对象的转换 + JSON 对象到领域模型的反序列化

  • 处理业务逻辑(实际需要我们写的代码)

  • 转储输出(领域模型到 JSON 对象的序列化+ JSON 对象到字符串的转换)

// inputjson json_input = json::parse("{""  \"_nested\": {""    \"_bool\": false,""    \"_int\": 0,""    \"_double\": 0,""    \"_string\": \"foo\"""  },""  \"_vector\": [{""    \"_bool\": true,""    \"_int\": 1,""    \"_double\": 1,""    \"_string\": \"bar\",""    \"_optional\": true""  },{""    \"_bool\": true,""    \"_int\": 2,""    \"_double\": 2.0,""    \"_string\": \"baz\",""    \"_optional\": false""  }]""}");
NestedStruct nested = json_input.get();// usenested.nested_.string_ +=" in nested struct";// outputjson json_output = json(nested);std::stringstring_output = json_output.dump(2);
  • 对于 JSON 对象和字符串之间的转换,主流的JSON 库都实现了:

    • 调用json::parse从字符串得到输入 JSON 对象

    • 调用json::dump将 JSON 对象转为用于输出的字符串

  • 而 JSON 对象和 C++ 结构体之间的转换,需要我们实现

    • 通过反序列化,调用json::get()得到NestedStruct nested

    • 通过序列化,使用nested构造输出 JSON 对象

实现

实现从 C++ 结构体到 JSON 的序列化/反序列化操作,需要用到以下信息:

  • 结构体有哪些字段

    • bool_/int_/double_/string_/optional_

    • nested_/vector_

  • 每个字段结构体中的什么位置

    • &SimpleStruct::bool_/&SimpleStruct::int_/&SimpleStruct::double_/&SimpleStruct::string_/&SimpleStruct::optional_

    • &NestedStruct::nested_/&NestedStruct::vector_

  • 每个字段JSON 中对应的名称是什么

    • "_bool"/"_int"/"_double"/"_string"/"_optional"

    • "_nested"/"_vector"

  • 每个字段如何从 C++ 到 JSON 进行类型映射

    • bool对应Booleanint对应Number(Integer)double对应Numberstring对应Stringvector对应ArraySimpleStruct/NestedStruct对应Object

    • 必选字段缺失或 字段类型与 JSON 数据类型不匹配,则抛出异常

    • 可选字段(例如optional_)缺失,则跳过检查

对于很多支持反射(reflection)的语言,JSON 的解析者可以通过反射接口,查询到SimpleStruct/NestedStruct所有的字段信息

尽管 C++ 支持运行时类型信息(RTTI, run-time type information),但无法得到所有上述信息,所以需要SimpleStruct的定义者把这些信息告诉JSON 的解析者

于是,我们用以下几种方法实现:

人工手写 序列化/反序列化 代码

动态反射

静态反射

编译器生成 序列化/反序列化 代码

人工手写 序列化/反序列化 代码

代码链接

实现序列化/反序列化最简单的方法,就是通过人工编写代码:

voidto_json(nlohmann::json& j,constSimpleStruct& value){
  j["_bool"] = value.bool_;
  j["_int"] = value.int_;
  j["_double"] = value.double_;
  j["_string"] = value.string_;
  j["_optional"] = value.optional_;
}voidfrom_json(constnlohmann::json& j, SimpleStruct& value){
  j.at("_bool").get_to(value.bool_);
  j.at("_int").get_to(value.int_);
  j.at("_double").get_to(value.double_);
  j.at("_string").get_to(value.string_);if(j.find("_optional") != j.cend()) {
    j.at("_optional").get_to(value.optional_);
  }
}voidto_json(nlohmann::json& j,constNestedStruct& value){
  j["_nested"] = value.nested_;
  j["_vector"] = value.vector_;
}voidfrom_json(constnlohmann::json& j, NestedStruct& value){
  j.at("_nested").get_to(value.nested_);
  j.at("_vector").get_to(value.vector_);
}
  • to_json/from_json包含了所有字段位置、名称、映射方法

    • 使用j[name] = field序列化

    • 使用j.at(name).get_to(field)反序列化

    • 针对可选字段检查字段是否存在,不存在则跳过

  • nlohmann 的 C++ JSON 库能处理结构嵌套

    • j = value.nested_会调用void to_json(json& j, const SimpleStruct& value)序列化SimpleStruct

    • j.get_to(value.nested_)会调用void from_json(const json& j, SimpleStruct& value)反序列化SimpleStruct

  • nlohmann 的 C++ JSON 库基于 C++ 原生的异常处理throw-try-catch):

    • 如果字段不存在,函数json::at抛出异常

    • 如果字段实际类型和 JSON 输入类型不匹配,函数json::get_to抛出异常

手写to_json/from_json需要写 2 份类似的代码:

  • 一方面,需要复制粘贴,导致代码冗余

  • 另一方面,两份代码逻辑不是对称的(需要特殊处理可选字段),不易于统一编写

动态反射

崇尚偷懒的 Google 的工程师为 chromium/base::Value构建了一套基于动态反射(dynamic reflection)的反序列化机制,实现统一的 JSON 数据和 C++ 结构体转换。(参考:chromium/base::JSONValueConverter

核心原理是:利用适配器模式(adapter pattern)策略模式(strategy pattern),定义接口(interface)抹除具体字段转换操作的类型,通过运行时多态(runtime polymorphism)调用接口进行实际的转换操作。

Talk is cheap, show me the code —— 代码链接

首先,为不同字段类型定义一个通用的转换接口ValueConverter,用于存储实际的 C++ 类型与 JSON 类型的转换操作(仅关联操作的字段类型,抹除具体转换操作的类型):

template<typenameFieldType>usingValueConverter =std::function<void(FieldType* field,conststd::string& name)>;
  • 参数field表示字段的值,name是字段的名称

  • 原始代码将ValueConverter定义为接口;本文为了化简,直接使用std::function(关于使用接口的讨论,参考:回调 vs 接口)

然后,为不同类型的结构体定义一个通用的转换接口FieldConverterBase,用于存储结构体内所有字段的转换操作(仅关联结构体的类型,抹除操作的字段类型):

template<typenameStructType>classFieldConverterBase{public:virtual~FieldConverterBase() =default;virtualvoidoperator()(StructType* obj)const=0;
};

接着,通过FieldConverter将上边两个接口承接起来,用于存储结构体字段类型的实际转换操作(类似于 double dispatch),同时关联上具体某个字段的位置和名称(实现FieldConverterBase接口,调用ValueConverter接口):

template<typenameStructType,typenameFieldType>classFieldConverter:publicFieldConverterBase {public:
  FieldConverter(conststd::string& name,
                 FieldType StructType::*pointer,
                 ValueConverter converter)
      : field_name_(name),
        field_pointer_(pointer),
        value_converter_(converter) {}voidoperator()(StructType* obj)constoverride{returnvalue_converter_(&(obj->*field_pointer_), field_name_);
  }private:std::stringfield_name_;
  FieldType StructType::*field_pointer_;
  ValueConverter value_converter_;
};
  • 构造时传递 字段名称field_name_,字段的成员指针(member pointer)(即字段位置)field_pointer_,字段的映射方法value_converter_

  • operator()转换时,调用value_converter_.operator(),传入 当前结构体中字段的值 和 字段的名称;其中结构体obj字段的值通过obj->*field_pointer_得到

最后,针对结构体定义一个存储所有字段信息(名称、位置、映射方法)的容器StructValueConverter,并提供注册字段信息的接口(有哪些字段)RegisterField和执行所有转换操作的接口operator()仅关联结构体的类型,利用FieldConverterBase抹除操作的字段信息):

template<classStructType>classStructValueConverter{public:template<typenameFieldType>voidRegisterField(FieldType StructType::*field_pointer,conststd::string& field_name,
                     ValueConverter value_converter){
    fields_.push_back(std::make_unique>(
        field_name, field_pointer,std::move(value_converter)));
  }voidoperator()(StructType* obj)const{for(constauto& field_converter : fields_) {
      (*field_converter)(obj);
    }
  }private:std::vector<std::unique_ptr>> fields_;
};

使用样例代码链接

具体使用时,只需要两步:

  1. 构造converter对象,调用RegisterField动态绑定字段信息(名称、位置、映射方法)

  2. 调用converter(&simple)对所有注册了的字段进行转换

// setup converter (partial)autoint_converter = [](int* field,conststd::string& name) {std::cout<< name <<": "<< *field <<std::endl;
};autostring_converter = [](std::string* field,conststd::string& name) {std::cout<< name <<": "<< *field <<std::endl;
};

StructValueConverter converter;
converter.RegisterField(&SimpleStruct::int_,"int",
                        ValueConverter<int>(int_converter));
converter.RegisterField(&SimpleStruct::string_,"string",
                        ValueConverter<std::string>(string_converter));// use converterSimpleStruct simple{2,"hello dynamic reflection"};
converter(&simple);// output://   int: 2//   string: hello dynamic reflection

基于动态反射的开源库:

  • https://github.com/fnc12/sqlite_orm

  • https://github.com/billyquith/ponder

  • https://github.com/rttrorg/rttr

静态反射

实际上,实现序列化/反序列化所需要的信息(有哪些字段,每个字段的位置、名称、映射方法),在编译时(compile-time)就已经确定了 —— 没必要在运行时(runtime)动态构建converter对象。所以,我们可以利用静态反射(static reflection)的方法,把这些信息告诉编译器,让它帮我们生成代码

核心原理是:利用访问者模式(visitor pattern),使用元组std::tuple记录结构体所有的字段信息,通过编译时多态(compile-time polymorphism)针对具体的字段类型进行转换操作。

Talk is cheap, show me the code —— 代码链接

首先,定义一个StructSchema函数模板(function template),返回所有字段信息(默认返回空元组):

template<typenameT>inlineconstexprautoStructSchema(){returnstd::make_tuple();
}

然后,提供DEFINE_STRUCT_SCHEMADEFINE_STRUCT_FIELD两个(macro),定义结构体字段信息(有哪些、位置、名称),隐藏StructSchemastd::tuple的实现细节:

defineDEFINE_STRUCT_SCHEMA(Struct, ...)        \
  template<>                                    \inlineconstexprautoStructSchema() { \using_Struct = Struct;                      \returnstd::make_tuple(__VA_ARGS__);         \
  }defineDEFINE_STRUCT_FIELD(StructField, StructName) \
  std::make_tuple(&_Struct::StructField, StructName)
  • StructSchema返回元组的结构是:((&field1, name1), (&field2, name2), ...)

    • DEFINE_STRUCT_SCHEMA定义了结构体Struct有哪些字段

    • DEFINE_STRUCT_FIELD定义了每个字段位置、名称

  • using _Struct = Struct提供了一种宏内数据接力的方法,让下一个宏能获取上一个宏的数据

最后,提供ForEachField函数,从对应的StructSchema取出记录结构体StructType所有字段信息的元组,然后遍历这个元组,从中取出每个字段的位置、名称,作为参数调用转换函数fn

template<typenameT,typenameFn>inlineconstexprvoidForEachField(T&& value, Fn&& fn){constexprautostruct_schema = StructSchema<std::decay_t>();
  detail::ForEachTuple(struct_schema, [&value, &fn](auto&& field_schema) {
    fn(value.*(std::get<0>(std::forward<decltype(field_schema)>(field_schema))),std::get<1>(std::forward<decltype(field_schema)>(field_schema)));
  });
}
  • fn接受的参数分别为:字段的值和名称(field_value, field_name)

    • 字段的值通过value.*field_pointer得到,其中field_pointer是成员指针

  • ForEachTuple的实现中还用到了静态断言(static assert)检查,具体见 代码

    • 检查StructSchema是否定义了字段信息

    • 检查每个字段的信息是否都包含了位置和名称

使用样例代码链接

具体使用时,也是需要两步:

  1. 使用DEFINE_STRUCT_SCHEMADEFINE_STRUCT_FIELD静态定义字段信息(名称、位置)

  2. 调用ForEachField并传入映射方法(函数模板或泛型 lambda 表达式),对所有字段调用这个函数

// define schema (partial)DEFINE_STRUCT_SCHEMA(
    SimpleStruct,
    DEFINE_STRUCT_FIELD(int_,"int"),
    DEFINE_STRUCT_FIELD(string_,"string"));// use ForEachTupleForEachField(SimpleStruct{1,"hello static reflection"},
             [](auto&& field,auto&& name) {std::cout<< name <<": "<< field <<std::endl;
             });// output://   int: 1//   string: hello static reflection

静态反射过程中,最核心的地方:传入ForEachField的函数fn,通过编译时多态针对不同字段类型选择不同的转换操作:

  • 针对int类型字段,ForEachField调用fn(simple.int_, "int")

  • 针对std::string类型字段,ForEachField调用fn(simple.string_, "string")

最后ForEachField(SimpleStruct{...}, [](...) { ... });经过内联(inline)后,生成的代码非常简单:

{
  SimpleStruct simple{1,"hello static reflection"};std::cout<<"int"<<": "<< simple.int_ <<std::endl;std::cout<<"string"<<": "<< simple.string_ <<std::endl;
}

基于静态反射的开源库:

  • https://github.com/qicosmos/iguana

使用编译时静态反射,相对于运行时动态反射,有许多优点:

动态反射静态反射
使用难度(难)需要编写注册代码,调用RegisterField动态绑定字段信息(易)可以通过声明式的方法,静态定义字段信息
运行时开销(有)需要动态构造converter对象,需要通过虚函数表(virtual table)实现面向对象的多态(无)编译时静态展开代码,和直接手写一样
可复用性(差)每个converter对象绑定了各个字段类型的具体映射方法;如果需要进行不同转换操作,则需要另外创建converter对象(好)在调用ForEachField时,映射方法作为参数传入;利用编译时多态的机制,为不同的字段类型选择合适的操作

编译器生成 序列化/反序列化 代码

代码链接

基于ForEachField,我们可以实现通用的结构体序列化/反序列化函数:

template<typenameT>structadl_serializer>> {template<typenameBasicJsonType>staticvoidto_json(BasicJsonType& j,constT& value){
    ForEachField(value, [&j](auto&& field,auto&& name) {
      j[name] = field;
    });
  }template<typenameBasicJsonType>staticvoidfrom_json(constBasicJsonType& j, T& value){
    ForEachField(value, [&j](auto&& field,auto&& name) {// ignore missing field of optionalif(::is_optional_v<decltype(field)> &&
          j.find(name) == j.end())return;

      j.at(name).get_to(field);
    });
  }
};
  • § 人工手写 序列化/反序列化 代码的代码类似:

    • 使用j[name] = field序列化

    • 使用j.at(name).get_to(field)反序列化

    • 针对可选字段检查字段是否存在,不存在则跳过(C++ 17 还可以使用if constexpr实现选择性编译)

  • 关于如何使用nlohmann::adl_serializer扩展自定义类型的序列化/反序列化操作,参考 How do I convert third-party types? | nlohmann/json

  • 使用的两个简单的变量模板(variable template),具体见 代码

    • has_schema检查是否定义了StructSchema

    • is_optional_v检查字段类型是不是可选参数

对于需要进行序列化/反序列化的自定义结构体,我们只需要使用DEFINE_STRUCT_SCHEMADEFINE_STRUCT_FIELD声明其字段信息即可 —— 不需要为每个结构体写一遍to_json/from_json逻辑了:

DEFINE_STRUCT_SCHEMA(
    SimpleStruct,
    DEFINE_STRUCT_FIELD(bool_,"_bool"),
    DEFINE_STRUCT_FIELD(int_,"_int"),
    DEFINE_STRUCT_FIELD(double_,"_double"),
    DEFINE_STRUCT_FIELD(string_,"_string"),
    DEFINE_STRUCT_FIELD(optional_,"_optional"));

DEFINE_STRUCT_SCHEMA(
    NestedStruct,
    DEFINE_STRUCT_FIELD(nested_,"_nested"),
    DEFINE_STRUCT_FIELD(vector_,"_vector"));

于是,编译器就可以生成和§ 人工手写 序列化/反序列化 代码一致的代码了。

图片来源:Declarative Programming And The Web

写在最后

不依赖于第三方库,只需要简单的声明,没有额外的运行时开销 —— 这就是现代 C++ 元编程

马上就 2019 年了,勤奋 的程序员还在加班手写重复代码的时候,懒惰 的程序员都去跨年了。。。😶

掌握 C++ 元编程,自己打造工具,解放生产力,告别搬砖的生活!

延伸阅读:

  • 浅谈 C++ 元编程 by BOT Man

  • Modern C++ 元编程应用 by 祁宇

  • C++ 反射的应用与实践 by 卜恪

如果有什么问题,欢迎交流。😄

Delivered under MIT License © 2018, BOT Man


左下角阅读原文 📃

右上角关注公众号 ❤ / 分享文章💡

右下角留言评论~ 🖊/ 觉得好看 👍

声明:本文部分素材转载自互联网,如有侵权立即删除 。

© 版权声明
THE END
喜欢就支持一下吧
点赞0赞赏 分享
相关推荐
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片

    暂无评论内容