原来一直对“反射”这个概念不清楚,听别人提起过很多次,也有vczh这样的大牛号称在C++里实现了反射,我也不知道具体指的是什么。今天在SOLID里遇到一个具体问题,跟HJK讨论了一下,发现原来这就是反射机制。

简单的说,反射就是让对象获取关于自身的信息(如对象类型),而不需要在编写代码的时候提前知道这些信息。比如说,我们有一个对象a,其类型是A。一般来说,我要对a进行操作,需要先声明一个A& a_pointer,然后再来做事情。但是假设我们这时候不知道A这个类型,或者这个类型的头文件并未被引用,就无法对a进行操作了。如果我们可以获得a的类型信息,比如TYPEOF(a)& a_pointer, 那么我们就无需知道a的具体类型,或者引用A类型的头文件了。

C++并不直接支持反射特性,但是上述的场景在OO语言中会经常出现。考虑一个常见的场景。假设有如下的两个派生类,都由Base类派生而来

// a_class.h
class A : public Base {
 public:
  A() {}
  virtual ~A() {}

  virtual void Produce(Base** x) {*x = new A;}
};
// b_class.h
class B : public Base {
 public:
  B() {}
  virtual ~B() {}

  virtual void Produce(Base** x) {*x = new B;}
};

其中的Produce函数可用来生成同类型的对象。同时我们有一个map<string,Base*> class_map。可以想象这个map的目的是为了用下标来代替具体的类型定义,如我们可以用class_map[“A”]来得到一个A类型的实例,从而使用class_map[“A”]->Produce(Base** x)来生成一个A类型的新对象。

但是,“A”字符串与A类型的对应关系是无处得知的,我们只能进行手动添加。简单的方法,就是在整个程序运行之前,对这个map进行各种insert(make_pair(“A”, (A*)a))。但是这显然是无法扩展的,这意味着每当有一个新的派生类出现,我们就需要修改这个函数,并且include这个新类型的头文件。需要重新编译的源文件数量也很多。更重要的,无法多人合作进行开发,这个insert函数的位置成为了竞争文件。

而反射机制就可以通过“A”字符串,或者其他的方法,来对已经存在的类型进行操作。

仔细考虑上面的例子,其重点就在于那个map需要被预先insert进去,而insert这个操作是无法在派生类声明的时候进行的。所以重点就是要找一个方法,在派生类声明之后,其他函数开始执行之前,执行将派生类的一个实例丢进map的操作。这里的第一个技巧是利用static变量,也就是,如果我在a_class.cc里的最开始写了一行static int some_int = 1;那么这个some_int确实被声明了(同时被初始化了),并且这个声明(初始化)的顺序是在main函数开始执行之前的;第二个技巧则是扩展这个初始化的操作,对一个变量进行初始化只能直接赋值,但是如果对一个类对象进行初始化,我们就有了构造函数这个大杀器了。考虑下面这个类

class AFactory {
 public:
  AFactory() {A* a = new A; class_map.insert("A", a);}
};

然后我们在a_class.cc的最前面,声明一个AFactory对象。

static AFactory a_factory;

这时一个A对象将被生成,并被丢进class_map里,其key为“A”。简单的看,我们在每个派生类里都建立这样一个Factory类,并在cc文件里加上声明即可。这样就与调用class_map的函数和文件完全解耦,增加再多的派生类也不需要去修改那边的代码了,只用修改当前派生类的代码就可以了。

实际上,如果是这种操作比较统一的Factory类,我们完全可以利用模版类来实现。考虑下面这个类

template 
class Factory {
 public:
  Factory(string name) {T* a = new T; class_map.insert(name, a);}
};

那么在a_class.cc里,只用这样声明

static Factory a_factory("A");

就搞定了。其他派生类同理。并不需要每个派生类都新建这样一个Factory类了。

对于SOLID这样需要强力扩展能力的系统(新协议和新应用非常多)来说,这个特性真是再好不过了。

大概研究了下,很多人使用宏定义来实现反射,我倒觉得模版类的方式更清晰点。实际上如果模版类的构造函数接受一个函数对象参数,那就想做什么都当参数仍进去就好了。所有initial类型的工作都可以一起做了。