跟我学c++高级篇——基础类型和POD结构体反射

一、基础类型反射

在前面以枚举体和函数进行了反射的实践,在其中也提到过,其实更应该进行反射的是类(结构体)。毕竟现在以面向对象编程基本已经普及,类(结构体)的应用几乎已经是无法避免的。所以对类(后面再提到类即包含结构体)的反射,其实应用的范围会更大。
对基础类型和POD类型的结构体进行反射,一般就是下面的情况:
1、对名称进行反射来动态获取和创建对象
2、对结构体变量的反射
这里只是POD类型的结构体,复杂的在后面再进行分析,当然这里也包含简单类。

二、实现机制

在前面反复进行反射的分析说明时,其实可以发现,反射实现常用的机制还是比较多的,但一般来说应用的比较多的就是使用模板和宏配合来实现。当然在具体的实现过程中可能需要一些具体的编译器或者辅助库的实现。另外在c++新标准下也提供了一些方法比如可以使用std::tuple来实现类的反射。
其实原理仍然是前面分析过的文章中的思路,即获取名称字符串与结构体之间的映射,其成员(变量)需要进行遍历注册。这样就可以在后面的代码中利用字符串来搞定这些对象获取及动态创建对象等。
这里需要再次说明的是:
1、是否需要侵入式处理
2、是否开销最小甚至是零开销
3、是否反射完全,即包括私有数据等是否可以得到
这三个条件是实现一个好的反射框架的前提。

三、实现

下面看一个初步的实现:

#pragma once
#include 
#include 
#include 
#include 

typedef unsigned int uint32;

struct Type {
	std::string typeName;
	size_t size;
};

//获得结构体成员类型信息
template<typename T>
 Type* GetFieldType();
#define GET_OBJECT_TYPE(TYPE) \
template<> \
Type* GetFieldType<TYPE>() { \
static Type t; \
  t.typeName = #TYPE; \
  t.size = sizeof(TYPE); \
  return &t; \
};\

//成员反射信息
struct Member {
	Type* t;
	std::string name;
	size_t offset;
};
constexpr int ARRAY_NUM = 4;

// 存储结构体的相关成员
struct ClassArray {
	std::array<Member, ARRAY_NUM> members;
};

//遍历获得类或结构体的成员信息
template<typename T>
const ClassArray* GetClass();
#define BEGIN_MEMBERS_FOR(CLASS)  \
template<> \
const ClassArray* GetClass<CLASS> () { \
  using ClassType = CLASS; \
  static ClassArray cArray; \
  enum { BASE = __COUNTER__ }; \

#define OBJECT_MEMBER(NAME)  \
  enum { NAME##ID = __COUNTER__ - BASE - 1}; \
  cArray.members[NAME##ID].t = GetFieldType<decltype(ClassType::NAME)>();\
  cArray.members[NAME##ID].name = { #NAME };  \
  cArray.members[NAME##ID].offset = offsetof(ClassType, NAME);\

#define END_MEMBERSS \
  return &cArray; \
};\

//获得类型信息宏处理
GET_OBJECT_TYPE(char)
GET_OBJECT_TYPE(short)
GET_OBJECT_TYPE(int)
GET_OBJECT_TYPE(uint32)

struct Data {
	int id;
	short len;
	char sex;
	uint32 size;
};

//处理结构体的反射
BEGIN_MEMBERS_FOR(Data)
OBJECT_MEMBER(id);
OBJECT_MEMBER(len);
OBJECT_MEMBER(sex);
OBJECT_MEMBER(size);
END_MEMBERSS

int main()
{
	auto x = GetClass<Data>();
	std::cout<<"TestStruct:"<<std::endl;
	for (auto &x :x->members){
		std::cout<<x.name<<",offset:"<<x.offset<<",type:"<<x.t->typeName<<std::endl;
	}
	return 0;
}

上面有两个宏需要注意,一个是__COUNTER__,一个是offsetof,前者是GNU的的一扩展,用来处理得到一个唯一键值;后者则是通过偏移量来获得类或结构体内成员的位置(也可以反过来理解)。
上面的代码其实很容易理解,注释写得比较清晰了,这里再简单的说明一下:
1、首先定义两个信息类:一个是基础的Type类,用来获得类型的相关信息,如名字等;另外一个是Member类,用来处理成员的相关信息
2、通过宏GET_OBJECT_TYPE来得到具体的类型信息,这个用来哪个就需要注册一个
3、通过宏OBJECT_MEMBER来得到成员信息并和刚刚得到的类型信息绑定
4、利用模板的显示实例化来实现不同的信息的具体处理
其实上面的代码只是一个基础的版本,如果不习惯使用宏的,可以将成员函数等宏实现改成普通的静态函数来实现。但是完全不使用宏,可能操作起来还是有些看起来不方便。这个根据个人的喜好自己斟酌。

四、std::tuple的实现

这个在这里简单介绍一下,回头在后续再展开分析。在前边的“面向切片编程”中,提到了使用tuple_element可以获得std::tuple中的类型。而在反射中,不就是一个类型获取的过程么?

//利用变参模板和tuple的特性来获取指定的类型
template<typename ...Types>
class ProxyObjectTypes {
  public:
  template<std::size_t ID>
  struct GetType {
    using Type = typename std::tuple_element<ID, std::tuple<Types...>>::type;
  };
};
//C++11定义的方式:
template <std::size_t I, class T>
using tuple_element_t = typename tuple_element<I, T>::type;

那么,这就可以通过显示实例化不同的模板函数参数版本来处理不同的数据类型,是不是和上面的代码中的类似。其实使用模板处理反射类型大多也是这么个路子。那么就可以写一个getType的模板函数,通过decay(decltype)和if constexpr来在编译期得到想要的数据类型。
可以通过下面的这个判断来确定std::tuple中的类型是否为自己需要的数据类型。这样就可以安全的获得类型信息的数据类型,进而得到数据对象。

constexpr int count_first_falses() { return 0; }

template <typename... T>
constexpr int count_first_falses(bool b, T... t)
{
  if (b)
  {
    return 0;
  }
  else
  {
    return 1 + count_first_falses(t...);
  }
}

template <typename E, typename... T>
decltype(auto) tuple_get_by_type(const std::tuple<T...>& tuple)    
{
  return std::get<count_first_falses((std::is_same<T, E>::value)...)>(tuple);
}

decltype(auto) getType(const std::tuple<int,int> &t)
{
  return tuple_get_by_type(t);
}

如果只是单纯的想获得类型可以用下面的方法:


#include 
#include 

template <typename T> T* getType() {  return nullptr;}
template <> int* getType<int>() { return new int; }
template <> std::string* getType<std::string>() { return new std::string; }

int main()
{
    using T = std::tuple<int, std::string>;
    getType<std::tuple_element<0, T>::type>();
    getType<std::tuple_element_t<1, T>>();

   return 0;
}

代码不难,基本上看一下就能明白。

五、总结

由简入繁,从基本到深入总是需要一个过程,学习反射也是如此。写代码是为了把学习的知识验证一下,同时可以更加深刻的理解知识。开发者们经常遇到这种场景,几行简单的代码,琢磨半天也确定不了什么意思,但是上机一调试,就恍然大悟。这就是侯捷教师的那句话“源码之前,了无秘密”。
反射还是有些难度的,所以这个系列会慢慢增加。

你可能感兴趣的:(c++)