使用宏实现c 反射功能,涉及c 知识点很多。
在很多程序设计中,经常会遇到这样的需求,即可以通过类的名字得到对应类型的对象,尤其是一种数据需要很多策略处理的时候。比如对于网页类型的识别,一篇网页可能是视频类型、新闻类型、图片类型、网站ag真人游戏首页、百科等很多类型中的一种,网页类型对于搜索引擎来说是非常重要的,计算rank的时候网页类型往往是一个非常重要的因子。具体实现的时候,网页类型识别的策略可以封装在类中,这样一个策略就可以设计成一个类。但是后期随着对网页理解的越来越深入,就会出现以下两种情景:
- 需要添加新的网页类型,因此需要添加对应的类型识别类;
- 有些类型已经不再需要或者是进行了重新划分,那么需要删除掉这些类型或者是让这些类型识别模块不再生效。
这种应用场景下,添加或移除网页类型识别模块时,最好能够非常方便,并且不会影响到已有的程序。
一个比较好的方案是,定义一个类型识别的基类pagetypedetector,每个类型识别策略都继承自这个基类。比如需要一个新闻页识别的新策略,那么定义类newspagetypedetector,该类继承pagetypedetector。在添加newspagetypedetector到网页类型识别的主程序时,在配置文件中进行配置,添加newspagetypedetector类,让该类生效,而主程序和其他类型识别策略的程序都不需要进行改动。另外,如果不再需要图片网页类型识别,那么就把图片类型识别对应的类名直接从配置发文件中删除即可。
为了实现上述目标,我们需要从类名到类型的映射,可以称为反射。因为配置文件中的信息在程序内部得到的都是纯字符串,程序需要根据字符串生成对应的识别类。当然,这个在本身已包含反射机制的程序设计语言中很容易实现,比如java,但是由于c 中语言本身不支持这种机制,因此,需要用其他的方法来模拟这种机制。
首先,我们从最简单的方式开始,定义一个工厂方法,该方法负责根据类名生成相应类的对象,函数定义可以如下:
pagetypedetector* detectorfactorycreate(const string& class_name);
生成新闻网页类型识别的类可以如下调用:
pagetypedetector* news_page_detector = detectorfactorycreate("newspagetypedetector");
detectorfactorycreate工厂方法中的实现逻辑大致是这样:
if (class_name == "newsdoctypedetector") { return new newsdoctypedetector; } else if (class_name == "...") { return new ...; }
使用如上工厂方法创建类的方式具有非常明显的缺陷,每添加或删除一个新类,都需要修改工厂方法内的程序(添加if判断或者删除if判断,并且需要添加新类的头文件或者类声明),当然了,因为程序有了修改所以就需要重新编译(如果很多其他模块依赖该程序的话,重新编译也是一笔不小的开销)。显然,这种方式虽然简单,但是极不易于维护。
这里,提出一个使用非常方便并且易于维护的ag真人游戏的解决方案,那就是使用宏。虽然c 创始人
bjarne stroustrup极力反对使用宏,但是在一些特定的场景中合理的使用宏会带来意想不到的效果。
首先,从使用宏最简单的一个实现开始,目标是可以通过类的名字得到相应的对象,因此应该有个方法类似于如下:
any getinstancebyname(const string& class_name);
返回值为any,因为不知道返回值究竟是什么类型,所以假定可以返回任何类型,这里的any使用的是boost中的any。该方法中需要new一个类型为class_name的对象返回,那么应该如何new该对象呢?借用上面使用工厂方法的经验,可以进一步使用工厂类,对于每个类,都有一个相应的工厂类objectfactoryclassname,由该工厂类负责生成相应的对象(为什么要使用工厂类?后面再作简单介绍)。
有了工厂类,也需要将类名与工厂类对应起来,对应方式可以使用map
负责将新类对应的工厂类添加到全局变量object_factory_map的函数必须在使用object_factory_map之前执行。gcc中有一个关键字__attribute__((constructor)) ,使用该关键字声明的函数就可以在main函数之前执行。到现在,程序的结构类似这样:
// 负责实现反射的文件reflector.h: mapobject_factory_map; any getinstancebyname(const string& name) { if (object_factory_map.find(name) != object_factory_map.end()) { return object_factory_map[name]->newinstance(); } return null; } #define reflector(name) \ class objectfactory##name { \ public: \ any newinstance() { \ return any(new name); \ } \ }; \ void register_factory_##name() { \ if (object_factory_map.find(#name) == object_factory_map.end()) { \ object_factory_map[#name] = new objectfactory##name(); \ } \ } \ __attribute__(constructor)void register_factory##name(); // 调用文件test.cc class testclass { public: void out() { cout << "i am testclass" << endl; } }; reflector(testclass); // main函数 int main() { any instance = getinstancebyname("testclass"); testclass* test_class = instance.any_cast (); return 0; }
到这里还有一个问题,全局变量objectfactorymap是不能放在头文件中的,因为如果多个类包含该头文件时,就会出现重复定义的错误,是编译不过的。因此,将该变量放在其源码reflector.cc文件中:
// reflector.h,包含声明: extern mapobject_factory_map; any getinstancebyname(const string& name); // reflector.cc: map object_factory_map; any getinstancebyname(const string& name) { if (object_factory_map.find(name) != object_factory_map.end()) { return object_factory_map[name]->newinstance(); } return null; }
上述程序编译能够通过,但是运行时出错,后来定位到是在使用全局变量object_factory_map时出错,经过调试了很久,在网上查相应的资料也没找到。经过不停的尝试,才发现原来是全局变量object_factory_map没有初始化,在仔细的测试了以后发现,是__attribute__((constructor))与全局变量类构造函数的执行顺序的问题,一般全局变量是在__attribute__(constructor)前完成初始化的,但是如果__attribute__是在main函数所在的文件,而全局变量是在其他文件定义的,那么__attribute__(constructor)就会在全局变量类构造函数前面执行,这样,上面的程序在全局变量类还没有完成初始化,也就是还没有执行构造函数,就在__attribute__(constructor)声明的函数中进行了使用,因此会出现问题。不过,在执行__attribute__时已经看到了全局变量的定义,只是没有执行全局变量的构造函数(这里,如果全局变量不是类,而是普通类型,是没有问题的)。所以,程序的结构还需要进一步修改。
现在解决如何定义和使用全局变量object_factory_map的问题。既然我们不能直接使用该变量,那么可以通过显示调用函数来返回该变量,如果直接在函数中new一个对象返回的话,那么每次调用都会new一个新的对象,而我们全局只需要一个该对象,这时该是static出现的时候了。我们可以这样定义:
// reflector.cc map& object_factory_map() { static map * factory_map = new map ; return *factory_map; }
这样定义还有另外一个优点,程序只是在真正需要调用g_objectfactory_map时才会生成相应的对象,而如果程序没有调用,也不会生成对应的对象。当然,在这里new一个对象的代价不大,但是如果new的对象非常耗时的话,这种使用函数中static变量代替全局变量方法的优势就非常明显了。到现在反射程序变成如下这样:
// 负责实现反射的文件reflector.h: // 工厂类的基类 class objectfactory { public: virtual any newinstance() { return any(); } }; map& object_factory_map(); any getinstancebyname(const string& name); #define reflector(name) \ class objectfactory##name : public objectfactory { \ public: \ any newinstance() { \ return any(new name); \ } \ }; \ void register_factory_##name() { \ if (object_factory_map().find(#name) == object_factory_map().end()) { \ object_factory_map()[#name] = new objectfactory##name(); \ } \ } \ __attribute__(constructor)void register_factory##name() // reflector.cc map & object_factory_map() { static map * factory_map = new map ; return *factory_map; } any getinstancebyname(const string& name) { if (object_factory_map().find(name) != object_factory_map().end()) { return object_factory_map()[name]->newinstance(); } return null; }
到现在接近尾声了,不过在很多时候,我们都是在已有基类的基础上添加新的类,就好比上述网页识别的程序,各个识别策略类都继承共同的基类,这样,我们可以进一步修改反射程序,将getinstancebyname放在另外一个类中,返回的是基类的指针,因此在定义基类时也需要注册一个宏,如下所示,同时需要修改objector_factory_map的结构为map
#define reflector_base(base_class) \ class base_class##reflector { \ public: \ static base_class* getinstancebyname(const string& name) { \ map& map = object_factory_map()[#base_class]; \ map ::iterator iter = map.find(name); \ if (iter == map.end()) { \ return null; \ } \ any object = iter->second->newinstance(); \ return *(object.any_cast ()); \ } \ };
这里就不再详细讲修改后的代码了,有兴趣的朋友可以自己实现。
注:
至于上面为什么需要使用工厂类,而不是直接new一个对应的对象返回,原因是直接new是不可以的。例如如下定义:
#define reflect(name) \ any getinstancebyname(const string& class_name) { return any(new name); }
如果是多个类使用的话,那么就会出现多个函数的定义。如果也借助工厂类的实现,如下实现:
#define reflect(name) \ any getinstancebyname##name(const string& class_name) { return any(new name); }
这样是不会出现重复定义了,但是这样在生产新的对象时需要指定特定的函数,这不又回到原点了吗?因此工厂类充当的是个中介的角色,我们可以保存工厂类,然后根据名称寻找特定的工厂类来生成对应的对象。
注:
为什么需要使用函数添加工厂类?因为在程序中,全局空间中只能是变量的声明和定义,而不能是语句,例如:
可以这样写:
int a = 10;
int main() {}
但是不能这样写:
int a;
a = 10;
int main() {}
需要注意的知识点:
- 工厂模式;
- 全局变量的定义需要注意,不能定义在头文件中(当如,如果经过特殊处理,例如使用#ifndef保护另说);
- any类型的实现;(准备写另外一篇文章来探讨其实现细节)
- 宏的定义以及使用;(基本覆盖了宏的所有知识)
- 全局变量构造函数与__attribute__((constructor))的执行顺序;(调试了很久)
- __attribute__((constructor))的问题;(编译器有关,放在函数定义前或定义后)
- 全局空间只能是声明或者定义,不能是语句;
- static在函数中的使用;
- 全局变量类的定义与使用。