#在C++中自动注册工厂机制一种方案的实现示例
以著名工业软件 OpenFOAM 为例,OpenFOAM 中包含各个 CFD 相关的模块,每个模块,从 C++ 的角度来看,其实都是一个类的框架。基类用作接口,一个派生类则是一个具体的模型。OpenFOAM 中的模块广泛使用 RTS 机制,而因此 OpenFOAM 的求解器中, 只需要设定模型的调用接口。算例具体使用的是那个模型,则是在运行时才确定的,而且可以在算例运行过程中修改选中的模型。
本文章将仿照OpenFOAM中的 RTS 机制的实现方式,来进行一套类似 RTS 机制的自动注册工厂的实现的简单示例,即 创建注册表→运行时往注册表内添加派生类→运行时查找与实例化注册表内的指定派生类并调用其内部函数 的整个过程。
本文中实现自动注册工厂机制的本质归纳
在进行代码实现之前,我们先来了解一下本文中实现自动注册工厂的示例的实质:
- 基类里定义一个
std::unordered_map
类型的algorithmTable
(即注册表,其中的每一组值都包含一个键值对),其 key 为类的algorithmName
(该algorithmName
为std::string
标准字符串类型,在查找 algorithmTable 注册表内的类时即搜索key) ,value 为一个函数对象,这个函数对象的返回值是基类类型的智能指针(本文采用std::unique_ptr
作为智能指针) ,并且这个智能指针指向类的一个临时对象(用 C++ 的std::make_unique
关键词创建 )。这些在基类(AlgorithmBase
)以及一个用于实现注册类(AlgorithmRegistrar
)中实现。 - 每创建一个派生类,都会调用一次
AlgorithmRegistrar
类中的构造函数。这个构造函数会触发一次algorithmTable
的更新操作。具体地说,这个构造函数的调用,会往基类里定义的algorithmTable
插入一组值,这组值的 key 是该派生类的algorithmname
,value 是一个函数对象,该函数对象返回的是指向派生类临时对象的基类类型的智能指针。 - 类及其派生类编译成库,在编译过程中,会逐步往
algorithmTable
增加新元素,直到可选的模型全部添加到其中。 - 在需要调用这些类的地方,只需要定义基类的智能指针,并用基类中定义的
GetAlgorithmTable()
函数来初始化获取algorithmTable
注册表,这样,就可以利用std::unordered_map
类型自带的 find 函数并根据调用时所提供的参数(即algorithmTable
的 key,也即派生类的algorithmName
),来从algorithmTable
中选择对应的派生类(即algorithmTable
的 value),从而可以实例化该派生类对象且获得指向此具体派生类对象的指针,并利用此指针来获取并调用对应派生类的函数。
经过以上四步,就实现了本文示例的自动注册工厂机制。
代码示例大纲
本文实现自动注册工厂机制代码示例的大纲如下:
- rts库
- rts.h 头文件
AlgorithmBase
模板基类- 定义
AlgorithmCreateFunc
函数类型 - 定义
AlgorithmBase()
构造函数接受两个不同类型的参数 - 定义虚函数
Execute()
在具体派生类中实现 - 定义
GetAlgorithmTable()
静态函数获取注册表 - 定义
RegisterAlgorithm()
函数实现注册功能
- 定义
AlgorithmRegistrar
注册表类- 定义
AlgorithmRegistrar()
构造函数
- 定义
- rts.cpp 源文件
AlgorithmA
派生类- 定义
AlgorithmA()
构造函数接受两个任意类型的参数 - 实现
AlgorithmA
类中的Execute()
函数
- 定义
- 使用
AlgorithmRegistrar
类将AlgorithmA
注册进algorithmTable
AlgorithmB
派生类- 定义
AlgorithmB()
构造函数接受两个任意类型的参数 - 实现
AlgorithmB
类中的Execute()
函数
- 定义
- 使用
AlgorithmRegistrar
类将AlgorithmB
注册进algorithmTable
- rts.h 头文件
- addc库
- addc.h 头文件
AlgorithmC
派生类- 定义
AlgorithmC()
构造函数接受两个任意类型的参数 - 声明
AlgorithmC
类中的Execute()
函数
- 定义
- 使用
AlgorithmRegistrar
类将AlgorithmC
注册进algorithmTable
- addc.cpp 源文件
- 给出
AlgorithmC
类中声明的Execute()
函数的具体实现
- 给出
- addc.h 头文件
- 主程序
- 设置命令行参数控制所选模型
- 使用基类中的
GetAlgorithmTable()
函数获取注册表 - 使用注册表的
find
函数搜索对应模型 - 传入参数对所选模型进行实例化
- 调用所选模型的
Execute()
函数
实现目的
将类及其派生类编译成库,在主程序内可直接通过获取与访问注册表来获取指定模型,并传入指定类型的参数对其进行实例化,再调用此模型的特定函数。在实际情况中,这个所选的派生类即为模型,派生类中特定的函数即为其特定的算法。这样做就可以只需要设定模型的调用接口。算例具体使用的是那个模型,则是在运行时才确定的,而且可以在算例运行过程中修改选中的模型
实现流程
实现流程即为上文所言的 创建注册表→更新注册表→引入与查找注册表来调动相应派生类 的整个流程。
创建注册表并将其编译成库
这里我们将实现注册表的库命名为rts,首先我们先看其代码实现。
rts.h头文件
#include <unordered_map>
#include <memory>
#include <string>
#include <functional>
#include <iostream>
//引入相应系统库
template<typename T1, typename T2>
//使用模板参数便于传入不同类型的参数
class AlgorithmBase {
public:
using AlgorithmCreateFunc = std::function<std::unique_ptr<AlgorithmBase<T1, T2>>(T1,T2)>;
//定义AlgorithmCreateFunc函数类型可接受函数对象并将其返回值转换为std::unique_ptr智能指针类型
//接受T1,T2模板参数是为了在主程序中对派生类进行任意类型的实例化
AlgorithmBase(T1 t1, T2 t2) : t1_(t1), t2_(t2){};
//定义AlgorithmBase构造函数
virtual void Execute() = 0;
//设定虚函数 Execute() 在具体派生类中实现
virtual ~AlgorithmBase() {};
//设定析构函数为虚函数以实现多态,可利用基类的智能指针实现临时派生类对象的销毁
static std::unordered_map<std::string, AlgorithmCreateFunc>& GetAlgorithmTable() {
static std::unordered_map<std::string, AlgorithmCreateFunc> algorithmTable;
return algorithmTable;
}
//定义注册表类型为std::unordered_map的GetAlgorithmTable()函数
//用于静态创建并返回std::unordered_map的注册表,这里返回了一个algorithmTable对象表示注册表
static void RegisterAlgorithm(const std::string& algorithmName, AlgorithmCreateFunc createFunc) {
GetAlgorithmTable()[algorithmName] = createFunc;
//往algorithmTable内添加键值对
//其key为std::string类型的派生类的name(algorithmName)
//value是返回值为指向具体派生类对象的基类类型的智能指针的函数对象
}
//定义RegisterAlgorithm()成员函数用于往algorithmTable注册表内添加键值对
private:
T1 t1_;
T2 t2_;
//声明构造函数传入变量
};
template <typename AlgorithmType, typename T1, typename T2>
//使用模板参数使得AlgorithmRegistrar接受三个任意类型的参数
class AlgorithmRegistrar {
public:
AlgorithmRegistrar(const std::string& algorithmName)
//定义AlgorithmRegistrar()构造函数接受派生类的name
{
AlgorithmBase<T1, T2>::RegisterAlgorithm(algorithmName, [](T1 t1, T2 t2) {
return std::make_unique<AlgorithmType>(t1, t2);
});
//调用基类中的RegisterAlgorithm()函数实现往algorithmTable(即std::unordered_map类型注册表)内添加一组键值对
//这里传入的第一个值algorithmName为派生类的name
//传入的第二个值为函数类型的lambda表达式
//该lambda表达式表示的匿名函数接受两个参数并将其传入具体派生类的构造函数中完成实例化
//然后再通过std::make_unique关键词返回指向该类的指针
}
};
rts.h 头文件内实现了运行时注册表所需的类及其函数,通过这些定义之后,我们后续只需引入rts.h头文件即可对其进行运行时模型的添加(即往 algorithmTable
注册表内添加键值对)。添加完模型后也可在主程序中通过引入rts.h头文件来获取algorithmTable
注册表以实现运行时选择模型的功能。
接着我们在 rts.cpp 源文件内通过创建具体的派生类来完成 algorithmTable
注册表的初始化并向其中注册 AlgorithmA
以及 AlgorithmB
两个派生类以作为初始模型。
rts.cpp 源文件
#include "rts.h"
#include <iostream>
template <typename T1, typename T2>
//使用模板参数以接受任意类型
class AlgorithmA : public AlgorithmBase<T1, T2> {
public:
AlgorithmA(T1 t1, T2 t2) : AlgorithmBase<T1, T2>(t1, t2) {}
//设定 AlgorithmA 类构造函数
void Execute() override {
std::cout << "Algorithm A is executed." << std::endl;
}
//重构基类中的 Execute() 函数,用以区分所选中的不同派生类
};
AlgorithmRegistrar<AlgorithmA<int,double>, int, double> algorithmARegistrar("AlgorithmA");
//实例化 AlgorithmRegistrar 类并传入类别参数为<int,double>类型
//实例化时传入 "AlgorithmA" 这个 AlgorithmA 派生类的 name 作为注册表的key进行注册
template <typename T1, typename T2>
class AlgorithmB : public AlgorithmBase<T1, T2> {
public:
//AlgorithmA() : AlgorithmBase(0, 0.0) {} // 默认构造函数
AlgorithmB(T1 t1, T2 t2) : AlgorithmBase<T1, T2>(t1, t2) {}
void Execute() override {
std::cout << "Algorithm B is executed." << std::endl;
}
};
AlgorithmRegistrar<AlgorithmB<float,double>, float, double> algorithmBRegistrar("AlgorithmB");
//AlgorithmB 同理,但 AlgorithmB 派生类接受<float,double>类型的两个参数进行实例化
rts.cpp 源文件内创建并注册了两个派生类 AlgorithmA
和 AlgorithmB
,也可认为创建并向注册表添加了两个模型,在第一次创建 的时候(即创建 AlgorithmA
派生类时)通过 AlgorithmRegistrar 类的构造函数间接调用了基类 AlgorithmBase
中的静态成员函数GetAlgorithmTable()
初始化了 algorithmTable
注册表并向其中添加了指向 AlgorithmA
派生类的键值对,其中的键(key)为 AlgorithmA
的 std::string
字符串类型的 algorithmName
,即为 "AlgorithmA"
,值(value)为一个返回值为一个指向 AlgorithmA
派生类对象的基类类型的智能指针(即std::unique_ptr<AlgorithmBase<T1, T2>
类型)的函数对象,在本文中的程序中即以 lambda
表达式来代表,即 [](T1 t1, T2 t2) { return std::make_unique<AlgorithmType>(t1, t2); }
; AlgorithmB
派生类的注册过程同理,只是其接受的构造函数的参数类型为 <float,double>
区别于 AlgorithmA
派生类的 <int,double>
类型。
注:本文使用 CMake 中的 add_library
指令将头文件与源文件编译为库
运行时往注册表内添加派生类
接下来我们定义 addc 库内的文件,我们希望 addc 库所实现的功能是在编译运行时往 algorithmTable
内添加新的派生类 AlgorithmC
,由于我们已经在 rts 库内进行过注册操作,因此在 addc 库中进行这样的操作是很简单的,我们只需这样定义 addc 库的头文件 addc.h:
addc.h 头文件
#ifndef ADDC_H
#define ADDC_H
#include <iostream>
#include "rts.h"
template<typename T1, typename T2>
class AlgorithmC : public AlgorithmBase<T1, T2> {
public:
AlgorithmC(T1 t1, T2 t2) : AlgorithmBase<T1, T2>(t1, t2) {}
void Execute() override {} //在头文件内声明
};
AlgorithmRegistrar<AlgorithmC<int,double>, int, double> algorithmCRegistrar("AlgorithmC");
#endif // ADDC_H
再于 addc.cpp 源文件内实现 Execute() 函数:
addc.cpp 源文件
#include "addc.h"
template<> //模板特化
void AlgorithmC<int,double>::Execute() {
std::cout << "Algorithm C is executed." << std::endl;
}
这就实现了往 algorithmTable
注册表内添加新的派生类的操作,再使用 CMake 的指令 add_library
即可将其编译为库存储。
接着便是主程序部分,在讲解主程序代码之前,我们先需要使用 CMake 中的 target_link_libraries
指令引入 rts 库和 addc 库的库文件,然后再使用 target_include_directories
引入他们的头文件目录,然后再于主程序内使用 #include rts.h
这一行代码即可完成自动工厂注册表的引入。
运行时查找与实例化注册表内的指定派生类并调用其内部函数
主程序在引入 rts 库之后只需要调用基类中的 GetAlgorithmTable() 函数即可获得自动注册工厂的注册表 algorithmTable
,然后通过注册表类型自带的 find
函数根据指定的(即命令行传入的参数) algorithmName
来选择对应派生类(即对应模型),然后调用其特有的 Execute() 函数即可。
main.cpp 主程序
#include <iostream>
#include <string>
#include <memory>
#include "rts.h"
//引入 rts 库
int main(int argc, char* argv[]) {
//设定主程序接受命令行参数个数argc,以及命令行参数内容数组argv
if (argc >= 2){
//判定是否传入了除调用主程序可执行文件外的命令行参数
std::string algorithmName = argv[1];
//获取命令行参数所指定的算法名字
if (algorithmName == "AlgorithmB"){
//如果是 AlgorithmB 则指定为<float,double> 类型
auto& algorithmTable = AlgorithmBase<float,double>::GetAlgorithmTable();
//从基类获取注册表 algorithmTable
auto algorithmIt = algorithmTable.find(algorithmName);
//使用注册表自带 find 函数获取 algorithmName 这个key对应的value
//即获取对应的lambda函数对象
if (algorithmIt != algorithmTable.end()) {
//判定注册表内是否存在此 algorithmName 对应的键值对
std::unique_ptr<AlgorithmBase<float,double>> algorithmPtr = algorithmIt->second(0.0,0.0);
//传入一个float类型和一个double类型的值实例化派生类
//使用 second 函数实例化派生类并获得指向其实例化的指针 algorithmPtr
algorithmPtr->Execute();
//使用派生类指针调用其 Execute() 函数
} else {
std::cout << "Invalid algorithm name." << std::endl;
//否则则输出无法找寻对应算法名信息
}
}
else {
//否则则为<int,double>类型
auto& algorithmTable = AlgorithmBase<int,double>::GetAlgorithmTable();
auto algorithmIt = algorithmTable.find(algorithmName);
if (algorithmIt != algorithmTable.end()) {
std::unique_ptr<AlgorithmBase<int,double>> algorithmPtr = algorithmIt->second(0,0.0);
传入一个int类型和一个double类型的值实例化派生类
algorithmPtr->Execute();
} else {
std::cout << "Invalid algorithm name." << std::endl;
}
}
}
else{
std::cout<<"请在./build/main 后面跟上对应算法名字"<<std::endl;
}
return 0;
}
只需要在调用主程序编译后产生的可执行文件时在命令行加上命令行参数即可指定调用对应算法的 Execute() 函数,如假设主程序编译后产生的可执行文件在当前目录下的build文件夹下,且此可执行程序名为 main ,输入./build/main AlgorithmC
即可实例化 AlgorithmC
类并调用其 Execute()
函数,正确结果应当会输出AlgorithmC is executed.
总而言之,本文只是对自动化注册工厂这个流程做了一个简单的示范,实际情况下会更为复杂,比如函数的设定、命令行参数的指定与判断、具体派生类的功能等等可以根据需要自主设定。
编译后产生的可执行文件在当前目录下的build文件夹下,且此可执行程序名为 main ,输入./build/main AlgorithmC
即可实例化 AlgorithmC
类并调用其 Execute()
函数,正确结果应当会输出AlgorithmC is executed.
总而言之,本文只是对自动化注册工厂这个流程做了一个简单的示范,实际情况下会更为复杂,比如函数的设定、命令行参数的指定与判断、具体派生类的功能等等可以根据需要自主设定。