C++下实现机制的反射「1」——自省

反射(Reflection)机制

是指计算机程序在运行时(runtime)可以访问、检测和修改它本身状态或行为的一种能力。wiki

在其他语言中都有大量的应用,尤其是Java中反射与注解配合使用可以大大的减少工作量,降低重复代码,也能使程序更容易维护。现代Java框架中已经离不开反射与注解了。

目标

最近在公司的QT项目中需要大量的使用数据库,虽然QT提供了非常方便的数据库连接驱动与API,但是并没有更方便的ORM功能,只能拼写Sql语句之后运行,再从结果中手动处理得到对象。

虽然这么做并没有什么问题,但是问题是当一个软件设计到一定程度,例如出现了N个需要存储的对象,如果按照Model,DAO,Service,Controller的MVC设计思路,每一个Model都需要手动为其写insert,update,delete的函数。到最后,同样功能的sql语句可能会写十几遍,所以需要设计一套能应对此类情况的框架。

所以,我们就要在C++环境下设计与实现反射机制以及ORM系统。

实现更高级的自省

C++中虽然没有反射机制,但是也是拥有基础的自省。可以在Runtime检查对象的类型,但也是仅仅如此。但是如果我们想要为一大堆的自定义类实现一个to_string()方法或者to_json_string()的话就得手动为每个类实现,为了节省这些重复劳作的代码和时间,所以首先我们得实现类成员的反射,使自定义对象能在运行过程中自己获得并知道对象中的成员变量及其值。

基础反射机制

在实现反射的机制上本人想到了两种实现方式:

  1. 在自定义类中维护一个成员变量Map,并实现一个set函数用于插入「成员变量」,但这里的成员变量并不是实际意义上的成员变量,对于该变量的操作都得使用get、set方法从成员变量Map中读写。虽然这样就无法使用Object.member的方法去读写变量,但是好处是能够在后期无损添加减少成员变量。
  2. 在自定义类中维护一个成员变量指针Map,实现一个set_field函数进行注册成员变量,这样的好处就是这是一个正常的类结构,虽然没有第一种方式那样能够随意的拓展成员变量,但是还是能使用Object.member去访问。

所以本人就基于第二种开始实现。

Map的实现

本人实现此机制主要是用于数据的映射,所以先可以实现简单的结构,主要以string,int,double类这几种类型就可以应对大部分场面。

#ifndef BASE_ENTITY_FIELD_TYPE
    #define BASE_ENTITY_FIELD_TYPE
    /// define m_table FIELD macro type
    /// #define FIELD pair<string, pair<int, void *>>
    typedef pair<string, pair<int, void*>> FIELD;
    /// define m_table VALUE macro type
    /// #define VALUE pair<int, void *>
    typedef pair<int, void*> VALUE;
    /**
    * \brief 标记:int类型
    */
    constexpr int INT_TYPE    = 0x00;
    /**
    * \brief 标记:double类型
    */
    constexpr int DOUBLE_TYPE = 0x01;
    /**
    * \brief 标记:string类型
    */
    constexpr int STRING_TYPE = 0x02;

#endif
 
class Base{
  public:
    Base();
    void operator = (const Base& base) = delete;
  protected:
    vector<string> member_list;
    map<string, pair<int, void*>> m_table;
      void set_field(initializer_list<string> fields, initializer_list<void*> vals, initializer_list<int> types){
      int i = 0;
      auto i_f = fields.begin();
      auto i_v = vals.begin();
      auto i_t = types.begin();
      member_list.clear();
      for(; i_f != fields.end(); i_f++, i_v++, i_t++, ++i)
      {
          if(*i_t == STRING_TYPE)
          {
              m_table.insert(FIELD(*i_f, VALUE(*i_t, *i_v)));
          }
          else if(*i_t == INT_TYPE)
          {
              m_table.insert(FIELD(*i_f, VALUE(*i_t, *i_v)));
          }
          else
          {
              m_table.insert(FIELD(*i_f, VALUE(*i_t, *i_v)));
          }

          member_list.push_back(*i_f);
      }

    };
}

这样就实现了一个简单的保存成员变量的容器,子类通过继承Base类并在初始化中使用set_field()插入子类的成员名,成员变量类型以及成员类型指针。使用void *指针就可以保存任意的数据类型指针,因为无法简单的通过void *来判断类型,所以需要额外的数据来判断类型。

而且由于容器中保存得失指针,所以Base类并不能直接进行浅拷贝,会造成野指针的情况,如果要拷贝对象的话只能在子类对象中重新初始化一遍容器。

这样就可以根据自己的需求在Base类中实现任意需求的成员方法。

使用

class User :public Base{
public:
  User(){
    set_field(
      {"username","id"},
      {&this->username,&this->id},
      {STRING_TYPE,INT_TYPE }
    );
  };
  string username;
  int id;
}

总结

维护通过维护一个指针表,我们就可以通过该表实现很多C++无法实现的功能,同时不仅仅是成员变量,成员方法也可以通过类似的方法注册进入Base类,实现更骚的操作。