OpenFOAM 中的 Run Time Selection 机制

source flux 博客 曾经出过一个解释 Run Time Selection(RTS) 机制的系列博文,推荐想理解 RTS 的读者去仔细读读。本篇算是我在读完以后做的一个笔记,以及一些总结,供读者参考。

OpenFOAM 中包含各个 CFD 相关的模块,每个模块,从 C++ 的角度来看,其实都是一个类的框架。基类用作接口,一个派生类则是一个具体的模型。OpenFOAM 中的模块广泛使用 RTS 机制,因此 OpenFOAM 的求解器中,只需要设定模型的调用接口。算例具体使用的是那个模型,则是在运行时才确定的,而且可以在算例运行过程中修改选中的模型。下面通过一个 source flux 博客 提供的代码,来解读 RTS 机制的实现原理。
为了方便解读,这里将代码摘录如下,代码所有权归 source flux 博客 所有:

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
#include "word.H"
#include "messageStream.H"
#include "argList.H"

using namespace Foam;

#include "typeInfo.H"
#include "runTimeSelectionTables.H"
#include "addToRunTimeSelectionTable.H"

// Main program:
class AlgorithmBase
{
public:

// Declare the static variable typeName of the class AlgorithmBase.
TypeName ("base");

// Empty constructor.
AlgorithmBase () {};

// Word constructor.
AlgorithmBase (const word& algorithmName) {};

// Destructor: needs to be declared virtual since
virtual ~AlgorithmBase() {};

// Macro for declaring stuff required for RTS
declareRunTimeSelectionTable
(
autoPtr,
AlgorithmBase,
Word,
(
const word& algorithmName
),
(algorithmName)
)

// static Factory Method (selector)
static autoPtr<AlgorithmBase> New (const word& algorithmName)
{

// Find the Factory Method pointer in the RTS Table
// (HashTable<word, autoPtr<AlgorithmBase>(*)(word))
WordConstructorTable::iterator cstrIter =
WordConstructorTablePtr_->find(algorithmName);

// If the Factory Method was not found.
if (cstrIter == WordConstructorTablePtr_->end())
{
FatalErrorIn
(
"AlgorithmBase::New(const word&)"
) << "Unknown AlgorithmBase type "
<< algorithmName << nl << nl
<< "Valid AlgorithmBase types are :" << endl
<< WordConstructorTablePtr_->sortedToc()
<< exit(FatalError);
}

// Call the "constructor" and return the autoPtr<AlgorithmBase>
return cstrIter()(algorithmName);

}

// Make the class callable (function object)
virtual void operator()()
{

// Overridable default implementation
Info << "AlgorithmBase::operator()()" << endl;
}
};

defineTypeNameAndDebug(AlgorithmBase, 0);
defineRunTimeSelectionTable(AlgorithmBase, Word);
addToRunTimeSelectionTable(AlgorithmBase, AlgorithmBase, Word);

class AlgorithmNew
:
public AlgorithmBase
{
public:

// Declare the static variable typeName of the class AlgorithmNew.
TypeName ("new");

// Empty constructor.
AlgorithmNew () {};

// Word constructor.
AlgorithmNew (const word& algorithmName) {};

// Make the class callable (function object)
virtual void operator()()
{

Info << "AlgorithmNew::operator()()" << endl;
}
};

defineTypeNameAndDebug(AlgorithmNew, 0);
addToRunTimeSelectionTable(AlgorithmBase, AlgorithmNew , Word);

class AlgorithmAdditional
:
public AlgorithmNew
{
public:

// Declare the static variable typeName of the class AlgorithmAdditional.
TypeName ("additional");

// Empty constructor.
AlgorithmAdditional () {};

// Word constructor.
AlgorithmAdditional (const word& algorithmName) {};

// Make the class callable (function object)
virtual void operator()()
{

// Call base operator explicitly.
AlgorithmNew::operator()();
// Perform additional operations.
Info << "AlgorithmAdditional::operator()()" << endl;
}
};

defineTypeNameAndDebug(AlgorithmAdditional, 0);
addToRunTimeSelectionTable(AlgorithmBase, AlgorithmAdditional , Word);

int main(int argc, char *argv[])
{

argList::addOption
(
"algorithmName",
"name of the run-time selected algorithm"
);

argList args(argc, argv);

if (args.optionFound("algorithmName"))
{
// Get the name of the algorithm from the arguments passed to the
// application.
const word algorithmName = args.option("algorithmName");

// RTS call.
autoPtr<AlgorithmBase> algorithmPtr = AlgorithmBase::New(algorithmName);

// Get the reference to the algorithm from the smart pointer.
AlgorithmBase& algorithm = algorithmPtr();

// Call the algorithm.
algorithm();
}
else
{
FatalErrorIn
(
"main()"
) << "Please use with the 'algorithmName' option." << endl
<< exit(FatalError);
}

Info<< "\nEnd\n" << endl;

return 0;
}

在解读原理之前,先来看看这段代码。可以发现,RTS 机制的实现跟几个函数的调用有关: declareRunTimeSelectionTabledefineRunTimeSelectionTabledefineTypeNameAndDebugaddToRunTimeSelectionTable。规律可以总结如下:

  1. 基类类体里调用 TypeNamedeclareRunTimeSelectionTable 两个函数,类体外面调用 defineTypeNameAndDebugdefineRunTimeSelectionTableaddToRunTimeSelectionTable 三个函数;
  2. 基类中需要一个静态 New 函数作为 selector
  3. 派生类类体中需要调用 TypeName 函数,类体外调用 defineRunTimeSelectionTableaddToRunTimeSelectionTable 两个宏函数。

以上函数,经过搜索,发现都是定义在 runTimeSelectionTables.HaddToRunTimeSelectionTable.H 两个头文件中,而且,这些函数都是宏函数。

看来,理解 RTS 的第一步就需要仔细看看这几个宏函数。

先来看基类中的宏函数 declareRunTimeSelectionTable ,根据 source flux 的博文,这个宏函数针对前面的那段代码的展开结果为:

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
typedef autoPtr< AlgorithmBase > (*WordConstructorPtr)( const word& algorithmName );

typedef HashTable< WordConstructorPtr, word, string::hash > WordConstructorTable;
static WordConstructorTable* WordConstructorTablePtr_;
static void constructWordConstructorTables();
static void destroyWordConstructorTables();
template< class AlgorithmBaseType >
class addWordConstructorToTable
{
public:
static autoPtr< AlgorithmBase > New ( const word& algorithmName )
{
return autoPtr< AlgorithmBase >(new AlgorithmBaseType (algorithmName));
}
addWordConstructorToTable ( const word& lookup = AlgorithmBaseType::typeName )
{
constructWordConstructorTables();
if (!WordConstructorTablePtr_->insert(lookup, New))
{
std::cerr<< "Duplicate entry "
<< lookup << " in runtime selection table "
<< "AlgorithmBase" << std::endl;
error::safePrintStack(std::cerr);
}
}

~addWordConstructorToTable()
{
destroyWordConstructorTables();
}
};
template< class AlgorithmBaseType >
class addRemovableWordConstructorToTable
{
const word& lookup_;

public:
static autoPtr< AlgorithmBase > New ( const word& algorithmName )
{
return autoPtr< AlgorithmBase >(new AlgorithmBaseType (algorithmName));
}
addRemovableWordConstructorToTable ( const word& lookup = AlgorithmBaseType::typeName )
: lookup_(lookup)
{
constructWordConstructorTables();
WordConstructorTablePtr_->set(lookup, New);
}

~addRemovableWordConstructorToTable()
{
if (WordConstructorTablePtr_)
{
WordConstructorTablePtr_->erase(lookup_);
}
}
};

注意,由于 declareRunTimeSelectionTable 是在基类类体里调用的,所以,以上内容都是在类体里的。这相当于在类体了定义了两个 typedef,一个静态数据成员,两个静态函数,还有两个类。
先来看这两个 typedef 。第一个,定义的是一个函数指针,这样定义的结果是, WordConstructorPtr 代表一个指向参数为 const word&,返回类型为 autoPtr< AlgorithmBase > 的函数指针。第二个好理解,将一个 key 和 value 分别为 wordWordConstructorPtrhashTable 定义了一个别名 WordConstructorTable
静态数据成员 WordConstructorTablePtr_ 是一个 WordConstructorTable 类型的指针。
两个静态成员函数,这里只是声明了,并且注意到在下面定义的两个类中用到了这两个函数。

继续看 defineRunTimeSelectionTable(AlgorithmBase, Word)。这个宏展开的结果为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
AlgorithmBase::WordConstructorTable* AlgorithmBase::WordConstructorTablePtr_ = __null;
void AlgorithmBase::constructWordConstructorTables() {
static bool constructed = false;
if (!constructed) {
constructed = true;
AlgorithmBase::WordConstructorTablePtr_ = new AlgorithmBase::WordConstructorTable;
}
};

void AlgorithmBase::destroyWordConstructorTables() {
if (AlgorithmBase::WordConstructorTablePtr_) {
delete AlgorithmBase::WordConstructorTablePtr_;
AlgorithmBase::WordConstructorTablePtr_ = __null;
}
};

这个宏函数的主要功能,是对 declareRunTimeSelectionTable 中定义的静态数据成员和两个静态函数进行了定义。首先对静态数据成员 WordConstructorTablePtr_ 初始化为 __null,然后 constructWordConstructorTables 函数将 WordConstructorTablePtr_ 指向一个动态分配的 WordConstructorTabledestroyWordConstructorTables 则是对指针 WordConstructorTablePtr_ 进行销毁。

接着, addToRunTimeSelectionTable(AlgorithmBase, AlgorithmBase, Word) ,这宏函数展开以后其实就一句话:

1
AlgorithmBase::addWordConstructorToTable< AlgorithmBase > addAlgorithmBaseWordConstructorToAlgorithmBaseTable_;

这个语句,定义了一个 addWordConstructorToTable 的对象,仅此而已。但是,注意在创建一个类的对象的时候,是要调用该类的构造函数的。回头看 addWordConstructorToTable 类的构造函数,有意思的地方出现了。这个类的构造函数中,首先调用了 constructWordConstructorTables 函数,即对指针 WordConstructorTablePtr_ 进行了初始化。然后,对 WordConstructorTablePtr_ 进行 insert 操作,即,往其指向的 hashTable 插入 key-value 对。这里的 key 是创建对象 addAlgorithmBaseWordConstructorToAlgorithmBaseTable_ 时代入的模板参数对应的 类的 typeName(这一句很长很绕,需要好好理解,因为很重要!),value 则是 New 函数。这个 New 函数,指的是定义在 addWordConstructorToTable 中的 New 函数。这个 New 函数非常重要,再写一遍:

1
2
3
4
static autoPtr< AlgorithmBase > New ( const word& algorithmName )
{
return autoPtr< AlgorithmBase >(new AlgorithmBaseType (algorithmName));
}

这个New 函数,返回的是一个 AlgorithmBaseType(这里是 AlgorithmBase ) 类型的临时对象的指针!对应这里的情形,现在可以知道这个 insert 操作将创建一个 “类的typeName — 返回类的临时对象的引用的函数” 映射对,并增加到 WordConstructorTablePtr_ 中(看来 ddToRunTimeSelectionTable 中创建一个 addWordConstructorToTable 类的对象,居然目的是为了调用其构造函数。)。如果 insert 操作失败(原因是想要插入的 key 与 hashTable 已有的重复了,所以每一个类都需要不同的 typeName!),就会报条目重复的错。

好了,看完了基类相关的,在往下看派生类。前文已讲,派生类只需要在类体里调用 TypeName,然后在类体外调用 addToRunTimeSelectionTable 。对于派生类 AlgorithmNew,我们来看其具体的调用语句是

1
addToRunTimeSelectionTable(AlgorithmBase, AlgorithmNew , Word);

展开的结果应该是

1
AlgorithmBase::addWordConstructorToTable< AlgorithmNew > addAlgorithmNewWordConstructorToAlgorithmBaseTable_;

注意,这里又创建了一个 addWordConstructorToTable 类的对象,只是这里代入的模板参数是 AlgorithmNew 。于是,调用类的构造函数时代入的模板参数也就变了,所以这时 New 函数返回的将是 AlgorithmNew 类的临时对象的指针。并且, AlgorithmNew 这个名字与其对应的 New 函数组成的映射对,也被 insertWordConstructorTablePtr_ 里面。

AlgorithmAdditional 这个类,虽然是继承自 AlgorithmNew ,但是也是间接继承 AlgorithmBase 。并且,在 AlgorithmAdditional 类的类体之后调用的宏函数 addToRunTimeSelectionTable(AlgorithmBase, AlgorithmAdditional , Word) ,依然是将构建的映射对添加到了同一个 hashTable 里。

最后,再来看一下 selector,即基类中定义的 New 函数。这个函数的返回值类型为 autoPtr<AlgorithmBase> ,参数为跟类的 typeName 一样,都是 word&。这个函数里面,首先定义了一个 hashTable 的迭代器 cstrIter ,利用迭代器来遍历搜索,看 WordConstructorTable 里面是否能找到参数 algorithmName 相符的 key 值,如果找不到,那就报错退出,并输出当前的 WordConstructorTable 中可选的项的名称(即 WordConstructorTablePtr_->sortedToc());如果找到了,那就返回这个 key 对应的 value。而 WordConstructorTable 的 value 是一个函数指针,所以 cstrIter() 返回的是 algorithmName 对应的那个 New 函数(不要跟基类 AlgorithmBase 中作为 selector 的 New 函数搞混了!)。进一步看, cstrIter()(algorithmName) 则表示的是函数调用了,传给函数的参数正是 algorithmName
所以, cstrIter()(algorithmName) 返回的是 autoPtr<AlgorithmBase> ,其指向的是 typeName = algorithmName 的类的对象!
这样就实现了 New 函数作为 selector 的功能!

所以,RTS 机制的本质可以总结如下:

  1. 基类里定义一个 hashTable,其 key 为类的 typeName ,value 为一个函数指针,这个函数指针指向的函数的返回值是基类类型的 autoPtr ,并且这个 autoPtr 指向类的一个临时对象(用 C++ 的 new 关键字创建 )。这些在宏函数 declareRunTimeSelectionTable 中完成。

  2. 每创建一个派生类,都会调用一次 addToRunTimeSelectionTable 宏函数。这个宏函数会触发一次 hashTable 的更新操作。具体地说,宏函数的调用,会往基类里定义的 hashTable 插入一组值,这组值的 key 是该派生类的 typeName ,value 是一个函数,该函数返回的是指向派生类临时对象的指针。

  3. 类及其派生类编译成库,在编译过程中,会逐步往 hashTable 增加新元素,直到可选的模型全部添加到其中。

  4. 在需要调用这些类的地方,只需要定义基类的 autoPtr,并用基类中定义的 New 函数来初始化,即 autoPtr<AlgorithmBase> algorithmPtr = AlgorithmBase::New(algorithmName);。这样, New 函数就能根据调用的时候所提供的参数(即 hashTable 的 key),来从 hashTable 中选择对应的派生类(即 hashTable 的 value)。

经过以上四步,就实现了 RTS 机制。

参考:source flux 的系列博文