在 C++ 中用全局对象的构造函数实现反射机制

在C++中利用全局对象的构造函数实现反射机制。

1. 反射机制

Java语言中,对于任意一个类在运行状态下,都能够知道这个类的所有属性和方法;对于任意一个类的实例,也都能够调用它的任意方法和属性;这种动态获取类的信息以及动态调用对象方法的功能称为反射机制。

反射机制在我们日常的编程当中应用的不是很多,但是在设计模式中反射一个非常强大的工具。尤其是在设计模式中的工厂模式中,在工厂类中反射机制的使用可以免除switch case繁杂编写和维护。通过反射,可以方便的通过字符串指定类名从而创建出具体的产品类。但是在C++中,类的生成是要完完全全在编译时指定的。它无法像Java一样在运行时动态的创建对象。因此在增加产品类时将无可避免修改工厂类,这不是我们所希望看到的。一旦修改,就意味着要重新进行测试。

因此,为了使得C++更易于修改并且减少测试的工作量。本文将使用全局对象的构造函数来实现一定程度的反射机制。

2. 方法概述

由于C++语言的特性,任何对象的创建在编译期都必须进行明确。所以为了实现一定程度的反射,本文所述方法的主要结构就是在程序 Main 函数之前将所有的类的信息注册到一个数据结构中。这样在 Main 函数开始运行之后就可通过访问该数据结构获取全部类的信息。

由于C++是不允许在函数体外进行语句编写的,如何在 Main 函数运行之前构建好所有类的信息库是主要需要解决的问题。因此全局对象的构造函数恰好可以满足我们的需要。全局对象生命周期作用于整个程序的运行期,因此全局对象的内存分配和初始化都在程序正式运行之前。

3. 引入反射机制的简单工厂模式

3.1 工厂类

3.1.1 a_factory.h文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#ifndef _A_FACTORY_
#define _A_FACTORY_

#include "a_interface.h"

#define REGISTER_TO_A_FACTORY(PRDT, PRDT_NAME) \
PRDT reg_##PRDT; \
a_factory fty_##PRDT(PRDT_NAME, (a_interface *)&reg_##PRDT);

class a_factory
{
public:
a_factory(const char *name, a_interface *obj);

public:
static a_interface * get_a(const char *name);
};

#endif

宏 REGISTER_TO_A_FACTORY 用于将产品类注册到该工厂中。从宏的定义中可以看出,对于每一个产品类申明了一个该产品类的全局对象。并将该全局产品对象的地址和产品名作为构造函数的参数又申明了一个全局工厂对象。该工厂对象负责该产品对象的注册。注册的具体实现见 3.1.2。

静态成员函数 get_a 用于通过指定注册的子类(产品)名从而获得子类对象(该产品)。

3.1.2 a_factory.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include "a_factory.h"
#include <iostream>
#include <cstring>

using namespace std;

static struct {
char name[128] = {0};
a_interface *ptr = NULL;
} registation[100];
static int reg_idx = 0;

a_factory::a_factory(const char *name, a_interface *obj)
{
string key(name);
strcpy(registation[reg_idx].name, name);
registation[reg_idx].ptr = obj;
reg_idx++;
}

a_interface * a_factory::get_a(const char *name)
{
a_interface *tgt = NULL, *ret = NULL;
int i = 0;

for (i=0; i<reg_idx; i++) {
if (strcasecmp(name, registation[i].name) == 0) {
return (a_interface *)registation[i].ptr->clone();
}
}

return NULL;
}

全局静态变量 registation 负责存储所有注册的对象,全局静态变量 reg_idx 记录注册的个数。本例中 registation 为长度为100的数组。故最多可以注册100个对象。

3.2 产品类

3.2.1 a_interface.h

1
2
3
4
5
6
7
8
9
10
11
12
13
#ifndef _A_INTERFACE_
#define _A_INTERFACE_

class a_interface
{
public:
virtual void output() = 0;

public:
virtual a_interface * clone() = 0;
};

#endif

output() 函数为主要的产品方法,子类通过重载后实现具体的产品功能。clone() 函数为反射机制中用于创建自身对象的方法。产品子类中都必须实现这两个函数。

3.2.2 a1 类

3.2.2.1 a1.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#ifndef _A1_
#define _A1_

#include "a_interface.h"

class a1 : public a_interface
{
public:
virtual void output();

protected:
virtual a_interface * clone();
};

#endif

子类(具体产品类)需要继承产品基类,并且实现其具体的产品方法,即output函数。

3.2.2.1 a1.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>
#include "a1.h"
#include "a_factory.h"

using namespace std;

REGISTER_TO_A_FACTORY(a1, "a1");

void a1::output()
{
cout<<"This a1\n";
}

a_interface *a1::clone()
{
return (a_interface *) new a1();
}

子类只需在其CPP文件中使用REGISTER_TO_A_FACTORY宏,将自己注册到 registation 中即可。该注册过程通过全局对象的构造函数完成,其运行于 Main 函数之前。

3.2.2.3 a2.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#ifndef _A2_
#define _A2_

#include "a_interface.h"

class a2 : public a_interface
{
public:
virtual void output();

protected:
virtual a_interface * clone();
};

#endif
3.2.2.4 a2.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>
#include "a2.h"
#include "a_factory.h"

using namespace std;

REGISTER_TO_A_FACTORY(a2, "a2");

void a2::output()
{
cout<<"This a2\n";
}

a_interface * a2::clone()
{
return (a_interface *) new a2();
}

3.2.3 main.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>
#include "a_factory.h"

using namespace std;

int main(int argc, char **argv)
{
a_interface *a = NULL;

if (argc != 2) {
cout<<"Input Error"<<endl;
return 1;
}

a = a_factory::get_a(argv[1]);
if (a == NULL) {
cout<<"Not Found"<<endl;
return 1;
}
a->output();
return 0;
}

3.2.4 CMakeLists.txt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
CMAKE_MINIMUM_REQUIRED(VERSION 2.8)

PROJECT(ref)

SET(DO_LIB_SRC ./a_factory.cpp
./a1.cpp
./a2.cpp)
SET(A_SRC ./main.cpp)

SET(CMAKE_CXX_FLAGS_DEBUG -g)

ADD_LIBRARY(${PROJECT_NAME} STATIC ${DO_LIB_SRC})
ADD_EXECUTABLE(a ${A_SRC})
TARGET_LINK_LIBRARIES(a -Wl,--whole-archive ${PROJECT_NAME} -Wl,--no-whole-archive)

3.3 编译运行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
1.当前目录下文件
$ ls
a_factory.cpp a_interface.h a1.h a2.h main.cpp
a_factory.h a1.cpp a2.cpp CMakeLists.txt

2.编译程序
$ mkdir build; cd build; cmake ../; make

3.测试
$ ./a.exe a1
This a1
$ ./a.exe a2
This a2
$ ./a.exe a3
Not Found

通过上面的例子我们可以看到,只需通过字符串指定子类名,便可以创建出具体的子类。通过调用其继承并实现的虚函数实现其具体的产品方法。

3.3 如何编写新产品子类

在 3.2 小节中,子类产品 a2 的写法与 a1 类似。一个新的产品子类添加的步骤如下:

  1. 创建a3.h, a3.cpp文件

  2. 使 a3 类继承a_interface 类

  3. 实现产品函数和 clone 函数

  4. 在a3.cpp 文件中使用宏REGISTER_TO_A_FACTORY注册该产品子类

  5. 将a3.cpp 添加到CMakeLists.txt

由以上步骤可以看出,当需要新增子类时,只需编写子类代码即可,不用修改任何已有代码。

3.3.1 a3.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#ifndef _A3_
#define _A3_

#include "a_interface.h"

class a3 : public a_interface
{
public:
virtual void output();

public:
virtual a_interface * clone();
};


#endif

3.3.2 a3.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>
#include "a_factory.h"
#include "a3.h"

using namespace std;

REGISTER_TO_A_FACTORY(a3, "a3");

void a3::output()
{
cout<<"This is a3\n";
}

a_interface * a3::clone()
{
return (a_interface *) new a3();
}

3.3.3 CMakeLists.txt

1
2
3
4
5
6
7
8
9
将:
SET(DO_LIB_SRC ./a_factory.cpp
./a1.cpp
./a2.cpp)
改为:
SET(DO_LIB_SRC ./a_factory.cpp
./a1.cpp
./a2.cpp
./a3.cpp)

3.3.4 编译运行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
1.当前目录下文件
$ ls
a_factory.cpp a_interface.h a1.h a2.h a3.h main.cpp
a_factory.h a1.cpp a2.cpp a3.cpp CMakeLists.txt

2.编译程序
$ mkdir build; cd build; cmake ../; make

3.测试
$ ./a.exe a1
This a1
$ ./a.exe a2
This a2
$ ./a.exe a3
This is a3

可以看到新增子产品类 a3 已经可以顺利运行。

4. 注意事项

如果将工厂类及其所有产品类生成一个静态库使用。则必须将整个静态库全部链接,否则将无法实现注册。因为所申明并定义的用于注册的全局对象并没有在外部使用,如果采用默认的链接方式,这些对象将不会存在于最终程序中。所以需要使用-Wl,—whole-archive参数。