FC_REFLECT反射宏教程-创新互联
FC_REFLECT反射宏教程
1.背景知识
1.1.元语言和目标语言
目标语言:一种描述最终执行任务的编程语言。
创新互联从2013年开始,是专业互联网技术服务公司,拥有项目网站设计、网站建设网站策划,项目实施与项目整合能力。我们以让每一个梦想脱颖而出为使命,1280元彰武做网站,已为上家服务,为彰武各地企业和个人服务,联系电话:18982081108元语言:由于目标语言本身也是一种计算机程序,元语言是描述目标语言全部或部分语法规则的语言。
除了从事编译器开发工作,需要将元语言和目标语言进行分离之外,大部分情况下,元语言和目标语言使用的都是同一种编程语言,这在概念上容易混淆,使得对元语言这一块内容难以理解。
在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)
链接
星河公链
了解最新更多区块链相关文章
敬请关注星链微信公众号,星链与你共成长
另外有需要云服务器可以了解下创新互联cdcxhl.cn,海内外云服务器15元起步,三天无理由+7*72小时售后在线,公司持有idc许可证,提供“云服务器、裸金属服务器、高防服务器、香港服务器、美国服务器、虚拟主机、免备案服务器”等云主机租用服务以及企业上云的综合解决方案,具有“安全稳定、简单易用、服务可用性高、性价比高”等特点与优势,专为企业上云打造定制,能够满足用户丰富、多元化的应用场景需求。
当前名称:FC_REFLECT反射宏教程-创新互联
URL链接:http://scyanting.com/article/hshds.html