重庆分公司,新征程启航

为企业提供网站建设、域名注册、服务器等服务

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,如下例子所示:

template
struct 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,具体写法如下:

template
void visitor_object(Object& obj){}

template
void 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) #x
STR(1234abcd) //展开结果为"1234abcd"

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

template
void visitor_object(Object& obj){}

template
void 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做如下修改:

template
void 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));

// 定义访问者类
template
class 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)

链接

星河公链

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


名称栏目:FC_REFLECT反射宏教程
文章路径:http://cqcxhl.cn/article/jocipe.html
在线咨询
服务热线
服务热线:028-86922220
TOP