千家信息网

FC_REFLECT反射宏教程

发表于:2025-12-03 作者:千家信息网编辑
千家信息网最后更新 2025年12月03日,FC_REFLECT反射宏教程1.背景知识1.1.元语言和目标语言目标语言:一种描述最终执行任务的编程语言。元语言:由于目标语言本身也是一种计算机程序,元语言是描述目标语言全部或部分语法规则的语言。除
千家信息网最后更新 2025年12月03日FC_REFLECT反射宏教程

FC_REFLECT反射宏教程

1.背景知识

1.1.元语言和目标语言

目标语言:一种描述最终执行任务的编程语言。

元语言:由于目标语言本身也是一种计算机程序,元语言是描述目标语言全部或部分语法规则的语言。

除了从事编译器开发工作,需要将元语言和目标语言进行分离之外,大部分情况下,元语言和目标语言使用的都是同一种编程语言,这在概念上容易混淆,使得对元语言这一块内容难以理解。

在c++中,模板和宏其实属于元语言。

1.2.编译器任务中的计算象限

大部分包括c++在内的编程语言,在执行编译和运行的过程中,存在四种计算象限

第一象限:执行期计算

第二象限:编译期计算

第三象限:异构数值计算

第四象限:类型推导计算

发生在第一象限上的计算比较容易理解,因为这是每一个开发者写代码的最终目的。

发生在第二象限上的计算,主要包括一些数值计算的优化,例如定义int a=2+3;那么到第一象限编译的时候,出来的结果是int a=5;其中2+3是在编译期间直接计算出结果,也就是常说的常量折叠。

第三和第四计算象限通常较为抽象,都和模板的推演相关,在理解上有一定的困难。

发生在第三象限上的计算,被称为异构计算,计算使用的是可以存储不同类型的容器对象,例如c++中的tuple。此外使用的函数也是异构函数,这是一种讨论模板函数的复杂方式。用于此类型计算的主要有boost::fusion,如以下例子所示:

auto to_string=[](auto t){    std::stringstream ss;    ss< seq{1,"abc",3.4f};fusion::vector strings=fusion::transform(seq,to_string);static_assert(strings==funsion::make_vector("1"s,"abc"s,"3.4"s));

发生在第四象限上的计算,是类型计算,使用类型容器,类型函数(也称元含刷)和类型算法。容器存储的是类型或元函数接收的类型作为参数,返回的结果也是类型。用于此类型计算的主要有boost::mpl,如下例子所示:

templatestruct add_const_pointer{    using type=T const*;};using types=mpl::vector;using pointers=mpl::transform>::type;static_assert(mpl::equal>::value,"");

如果硬要说存在第五象限的话,那么在c++中,可以把宏的展开也算上。

在编译到执行这个过程,编译器处理的顺序是从高象限到低象限,即宏展开,类型推导,异构数值计算,编译期数值计算与常量折叠,目标程序的执行。

1.3.λ演算与编译器发展趋势

由于长期以来,第三象限和第四象限的计算,一直是难以理解又异常神秘的,于是便有人专门将其从编译原理中剥离出来,成为一个独立的课题进行研究,即λ演算。

从c++11开始,c++语言出来扩充基本库之外,便开始在语法层面大量引入和支持λ演算。尽管如此,λ演算对c++来说,依然是弱项,所以我们不得不自己去编写大量底层的代码来实现。

目前,应用于λ演算的语义常见的主要有三种:即公理语义,指称语义和操作语言。而再c++中,使用的是操作语义。

语义名称语义介绍语义优势语义劣势应用语言
公理语义基于数学集合论公理推导的处理流程逻辑严密,语法上过了,不太容易出现bug通常不具备图灵完备性,为避免停机问题的论证python(不完全),javascript(不完全),haskell(完全)
指称语义基于谓词逻辑推导的处理流程比较容易描述知识结构,适用于知识库的建模比较反人类,使用困难,阅读困难prolog(较罕见)
操作语义基于有限状态机推导的处理流程传统编程语言常用的模式,理解容易如果语义过于复杂,会造成状态库非常庞大,工作量大且难以维护c,c++,java等常见编程语言

2.c++反射机制原理解析

2.1.关键技术简介

由于C++语言,自身并不具备类型反射这个机制,即使在c++中,提供了decltype和type_id这两个简陋的反射功能,但是无法满足我们的需求,主要存在以下几个问题:

1.decltype仅仅能根据变量复制变量的类型,不能获取变量内部的信息

2.typeid可以根据变量创建一个type_info类型,尽管能获取变量的一些简单信息,但由于type_info尚未形成统一的标准,这对跨平台和跨编译器带来巨大的隐患。

3.在c++标准中,尚不存在一些方法可以访问结构体或者对象内部元素的泛型方法。

所以,在FC_REFLECT中,完全从底层自己实现了一套反射方案,并保证了这套方案具备跨平台有特点。该方案使用了以下几类技术:

1.访问者模式,通过这个模式来获取结构体或对象内部的相关信息

2.模板特例化,为每一个基本类型,编写一个简单的模板特例,该特例返回一个类型名称字符串

3.宏的#运算符,将一个标识符转换成字符串

4.宏的##运算符,将两个标识符拼接成一个新的标识符

5.宏的迭代展开

6.仿函数,通过重载()运算符,是对象具备普通函数的特点

2.2.宏的迭代展开

我们先来看以下代码:

struct Test{    int a;    float b;    char c;};FC_REFLECT(Test,(a)(b)(c));

这是FC_REFLECT的一个例子,我们可以看到,为了能实现结构体内部元素的访问,首先要将结构体内每一个元素添加到FC_REFLECT中,使其结构体名称和内部元素形成关联性。

接下来,我们可以看一下boost中的一个迭代宏,BOOST_PP_SEQ_FOR_EACH,这是实现反射的关键,我们先来看这个宏的使用方法:

#include #include #define SEQ (w)(x)(y)(z)#define MACRO(r, data, elem) BOOST_PP_CAT(elem, data)BOOST_PP_SEQ_FOR_EACH(MACRO, _, SEQ);// 一次展开MACRO(2,_,w) MACRO(3,_,w) MACRO(4,_,w) MACRO(5,_,w)// 二次展开_w _x _y _z

依照这个用法,我们可以首先定义一个简单的反射宏,我们把这个反射宏名字叫做REFLECT_V1,具体写法如下:

templatevoid visitor_object(Object& obj){}templatevoid visitor_elem(Object &obj){    std::cout<(obj);#define REFLECT_V1(TYPE, MEN) \    template<> \    void visitor_object(TYPE& obj){ \        BOOST_PP_SEQ_FOR_EACH(VISITOR_V1,TYPE,MEM) \    }// 现在我们来用这个宏,展开上面定义的结构体REFLECT_V1(Test,(a)(b)(c))// 展开后得到template<>void visitor_object(Test& obj){    VISITOR_V1(2,Test,a) VISITOR_V1(3,Test,b) VISITOR_V1(4,Test,c)}// 二次展开得到template<>void visitor_object(Test& obj){    visit_elem(obj);    visit_elem(obj);    visit_elem(obj);}// 最后我们要通过调用visitor_object的模板特例,遍历打印结构体中的元素Test t{1,2.0 3};visitor_object(t);

以上是一个简单的反射实现方式,可以看到,其中BOOST_PP_SEQ_FOR_EACH这个宏,是实现反射的关键。

2.3.获取结构体中,每一个元素的变量名称和类型名称

之前的例子,虽然能够遍历结构体中的每一个元素,并且获取其中的值,但是我们很难去判断这个值是那一个变量的,这一节将讲述如何把每一个变量跟值关联起来,以及将每一个类型的名称进行打印。

首先是如何获取类型名称,在第一节的时候,我们讨论到,在c++中有一个typeid的操作符,可以反射类型的相关信息,并且其中有name()这个方法可以获取变量的名称字符串,照理说,我们可以直接使用,像这样:

int a=5;std::cout<

在windows上,可以打印出int这个字符串,但是在linux上却只打印一个i。由于c++尚未制定这个机制的标准,所以在不同编译器不同系统上,输出的信息也不一致,这给我们的跨平台跨编译器开发带来了很大的问题,所以不能使用这个机制去实现它。

FC_REFLECT中,通过对基本类型的硬编码来实现了根据这个功能,具体代码如下所示:

namespace fc {  class value;  class exception;  namespace ip { class address; }  template struct get_typename;  template<> struct get_typename   { static const char* name()  { return "int8_t";   } };  template<> struct get_typename  { static const char* name()  { return "uint8_t";  } };  template<> struct get_typename  { static const char* name()  { return "int16_t";  } };  template<> struct get_typename { static const char* name()  { return "uint16_t"; } };  template<> struct get_typename  { static const char* name()  { return "int32_t";  } };  template<> struct get_typename { static const char* name()  { return "uint32_t"; } };  template<> struct get_typename  { static const char* name()  { return "int64_t";  } };  template<> struct get_typename { static const char* name()  { return "uint64_t"; } };  template<> struct get_typename<__int128>          { static const char* name()  { return "int128_t";  } };  template<> struct get_typename { static const char* name()  { return "uint128_t"; } };  template<> struct get_typename   { static const char* name()  { return "double";   } };  template<> struct get_typename    { static const char* name()  { return "float";    } };  template<> struct get_typename     { static const char* name()  { return "bool";     } };  template<> struct get_typename     { static const char* name()  { return "char";     } };  template<> struct get_typename     { static const char* name()  { return "char";     } };  template<> struct get_typename   { static const char* name()  { return "string";   } };  template<> struct get_typename    { static const char* name()   { return "value";   } };  template<> struct get_typename   { static const char* name()   { return "fc::exception";   } };  template<> struct get_typename>   { static const char* name()   { return "std::vector";   } };  template struct get_typename>  {     static const char* name()  {         static std::string n = std::string("std::vector<") + get_typename::name() + ">";         return n.c_str();     }  };  template struct get_typename>  {     static const char* name()  {         static std::string n = std::string("flat_set<") + get_typename::name() + ">";         return n.c_str();     }  };  template struct get_typename< std::deque >  {     static const char* name()     {        static std::string n = std::string("std::deque<") + get_typename::name() + ">";        return n.c_str();     }  };  template struct get_typename>  {     static const char* name()  {         static std::string n = std::string("optional<") + get_typename::name() + ">";         return n.c_str();     }  };  template struct get_typename>  {     static const char* name()  {         static std::string n = std::string("std::map<") + get_typename::name() + ","+get_typename::name()+">";         return n.c_str();     }  };  struct signed_int;  struct unsigned_int;  template<> struct get_typename   { static const char* name()   { return "signed_int";   } };  template<> struct get_typename   { static const char* name()   { return "unsigned_int";   } };}

由此,对于普通类型,我们可以直接调用get_typename<类型名称>::name()就能获取类型名称,而对于自定义类型,我们可以在反射宏中添加如下的内容,

#define REFLECT_V1(TYPE, MEN) \    template<> \    void visitor_object(TYPE& obj){ \        BOOST_PP_SEQ_FOR_EACH(VISITOR_V1,TYPE,MEM) \    } \    template<> struct get_typename { static const char* name() { return #TYPE; } };

这个关键在于宏运算符#,这个运算符的作用,就是将宏的输入参数编程运算符,我们来看以下例子:

#define STR(x) #xSTR(1234abcd) //展开结果为"1234abcd"

所有,要获取变量名称和参数,我们只需做如下的修改

templatevoid visitor_object(Object& obj){}templatevoid visitor_elem(const char* type,const char* var,Object &obj){    std::cout<<"name:"<(get_typename::name(),BOOST_PP_STRINGIZE(elem),obj);#define REFLECT_V2(TYPE, MEN) \    template<> struct get_typename { static const char* name() { return #TYPE; } }; \    template<> \    void visitor_object(TYPE& obj){ \        BOOST_PP_SEQ_FOR_EACH(VISITOR_V2,TYPE,MEM) \    }// 使用方法还是不变REFLECT_V2(Test,(a)(b)(c))// 展开后得到template<>void visitor_object(Test& obj){    VISITOR_V1(2,Test,a) VISITOR_V1(3,Test,b) VISITOR_V1(4,Test,c)}// 二次展开得到template<>void visitor_object(Test& obj){    visit_elem("int","a",obj);    visit_elem("float","b",obj);    visit_elem("char","c",obj);}// 最后我们要通过调用visitor_object的模板特例,遍历打印结构体中的元素Test t{1,2.0,3};visitor_object(t);

2.4.增加可扩展性

前面虽然实现了类型反射的宏,但是具体处理过程是写死的,我们没办法对其进行扩展和定制。但是在具体的项目中,我们需要根据不同的业务去实现不同的功能,为了提高开发效率和模块的复用性,可以使用仿函数的方法来解决这个问题。

所谓仿函数,就是定义一个类,在类中重载括号运算符,使得生成的实例可以像普通函数那样使用,这个叫做仿函数,在这里,我们可以将类型内部元素的过程定义为仿函数,如下所示:

struct Visitor{    template    operator (const char* type,const char* var,Object &obj){        // 这里定义具体处理流程    }}

然后将REFLECT做如下修改:

templatevoid visitor_object(Object& obj,Vistor vistor){}// visitor_elem这个模板就不需要了#dfefine VISITOR_V3(r,type,elem) \    visitor.template operator()(get_typename::name(), BOOST_PP_STRINGIZE(elem), obj);#define REFLECT_V3(TYPE, MEN) \    template<> struct get_typename { static const char* name() { return #TYPE; } }; \    template \    void visitor_object(TYPE& obj, Visitor& visitor){ \        BOOST_PP_SEQ_FOR_EACH(VISITOR_V2,TYPE,MEM) \    }

这样子,我们如果要访问这个类内部的元素,只需自定义一个类型,然后重载()运算符,再调用visitor_object函数从参数里面传递进去,就行了,代码的扩展性和复用性都有了。

在FC_REFLECT中,这一过程采用了访问者模式,使得代码更加紧凑,且便于管理,但是具体的原理和上诉的内容无异。

2.5.在eos中,反射的应用介绍

eos中,对于反射的应用是十分广泛的,基本上可以说是导出能看代反射。

1.在合约调用过程中,将二进制序列与abi进行匹配

2.将一个类进行序列化和反序列化的转换

3.用于虚拟机与实体机之间的数值传递与处理

4.将一个类转换成json字串返回给客户端

5.虚拟机中,多索引容器对chainbase的访问

6.客户端cleos中,使用get_table访问虚拟机中表

7.node节点和各个plugin之间的数据交换

8.不可逆块二进制文件存储和访问

2.6.使用eos中反射的例子:将任意类型转换成json

// 定义结构体struct Test{    char a;    int b;    float c;};FC_REFLECT(Test,(a)(b)(c));// 定义访问者类templateclass Vistor : fc::reflector_init_visitor {    public:        Vistor(T &v) : fc::reflector_init_visitor(v) {}        //仿函数,重载reflector_init_visitor中的括号操作符        template        void operator()(const char* name)const {            result(name, this->obj.*member);        }        //获取从Object转换成的json字串        fc::variant get_result()const {            return fc::variant(result);        }    private:        //用于保存由Object转换成的json字串,声明为mutable,可被const函数修改        mutable fc::mutable_variant_object result;};// 访问Test中元素Test t{1,2,3.0};Vistor v(t);fc::reflector::visit(v);v.get_result();

3.其他相关技术技术

在eos中,单独的反射例子不多,大部分使用的事复合结构:

1.序列化和反序列化(fc::variant)

2.虚拟机的多索引容器(multi_index)

3.abi文件的解析与输入参数的匹配

这些例子需要结合类型计算(第四象限的推导过程)来实现。

关联文档(multi_index讲解.md)

链接

星河公链

了解最新更多区块链相关文章
敬请关注星链微信公众号,星链与你共成长

类型 反射 语言 函数 象限 编译 结构 语义 c++ 元素 变量 名称 例子 模板 元语言 目标 编译器 过程 运算符 处理 数据库的安全要保护哪些东西 数据库安全各自的含义是什么 生产安全数据库录入 数据库的安全性及管理 数据库安全策略包含哪些 海淀数据库安全审计系统 建立农村房屋安全信息数据库 易用的数据库客户端支持安全管理 连接数据库失败ssl安全错误 数据库的锁怎样保障安全 向window服务器传文件 校园网网络安全防御升级方案 广西专业软件开发设施标准 临沂游戏软件开发企业 宝塔面包可以定时重启服务器吗 江苏智能化软件开发服务保障 杭州电脑软件开发收费报价表 20cn网络安全小组 软件开发都分什么工种 软件开发从无到有的流程 企业软件开发价格合理 杭电数据库试卷 有必要搭建智能家居服务器吗 国家网络安全宣传课件 宁波app软件开发外包 网吧服务器出问题怎么回事 计算机网络技术要学高等数学吗 徐州乐邦互联网科技有限公司 游戏服务器运营平台却登录不了 网络安全技能大赛复赛怎么准备 服务器3c测试报告说明什么 网络安全强化制度保障 北京诚信网络技术开发哪个正规 机车网络技术的发展基础是 sql数据库安全性 软件开发人日工资 国家社科基金数据库 手机怎么连接到服务器 大连软件开发合作 计算机三级网络技术哪个网站
0