source flux 博客 曾经出过一个解释 Run Time Selection(RTS) 机制的系列博文,推荐想理解 RTS 的读者去仔细读读。本篇算是我在读完以后做的一个笔记,以及一些总结,供读者参考。
OpenFOAM 中包含各个 CFD 相关的模块,每个模块,从 C++ 的角度来看,其实都是一个类的框架。基类用作接口,一个派生类则是一个具体的模型。OpenFOAM 中的模块广泛使用 RTS 机制,因此 OpenFOAM 的求解器中,只需要设定模型的调用接口。算例具体使用的是那个模型,则是在运行时才确定的,而且可以在算例运行过程中修改选中的模型。下面通过一个 source flux 博客 提供的代码,来解读 RTS 机制的实现原理。
为了方便解读,这里将代码摘录如下,代码所有权归 source flux 博客 所有:
| 1 | #include "word.H" | 
在解读原理之前,先来看看这段代码。可以发现,RTS 机制的实现跟几个函数的调用有关: declareRunTimeSelectionTable, defineRunTimeSelectionTable, defineTypeNameAndDebug, addToRunTimeSelectionTable。规律可以总结如下:
- 基类类体里调用 TypeName和declareRunTimeSelectionTable两个函数,类体外面调用defineTypeNameAndDebug,defineRunTimeSelectionTable和addToRunTimeSelectionTable三个函数;
- 基类中需要一个静态 New函数作为selector。
- 派生类类体中需要调用 TypeName函数,类体外调用defineRunTimeSelectionTable和addToRunTimeSelectionTable两个宏函数。
以上函数,经过搜索,发现都是定义在 runTimeSelectionTables.H 和 addToRunTimeSelectionTable.H 两个头文件中,而且,这些函数都是宏函数。
看来,理解 RTS 的第一步就需要仔细看看这几个宏函数。
先来看基类中的宏函数 declareRunTimeSelectionTable ,根据 source flux 的博文,这个宏函数针对前面的那段代码的展开结果为:
| 1 | typedef autoPtr< AlgorithmBase > (*WordConstructorPtr)( const word& algorithmName ); | 
注意,由于 declareRunTimeSelectionTable 是在基类类体里调用的,所以,以上内容都是在类体里的。这相当于在类体了定义了两个 typedef,一个静态数据成员,两个静态函数,还有两个类。
先来看这两个 typedef 。第一个,定义的是一个函数指针,这样定义的结果是, WordConstructorPtr 代表一个指向参数为 const word&,返回类型为 autoPtr< AlgorithmBase > 的函数指针。第二个好理解,将一个 key 和 value 分别为 word 和 WordConstructorPtr 的 hashTable 定义了一个别名 WordConstructorTable 。
静态数据成员 WordConstructorTablePtr_ 是一个 WordConstructorTable 类型的指针。
两个静态成员函数,这里只是声明了,并且注意到在下面定义的两个类中用到了这两个函数。
继续看 defineRunTimeSelectionTable(AlgorithmBase, Word)。这个宏展开的结果为:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15AlgorithmBase::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_ 指向一个动态分配的 WordConstructorTable 。 destroyWordConstructorTables 则是对指针 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
4static 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 函数组成的映射对,也被 insert 到 WordConstructorTablePtr_ 里面。
而 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 机制的本质可以总结如下:
- 基类里定义一个 - hashTable,其 key 为类的- typeName,value 为一个函数指针,这个函数指针指向的函数的返回值是基类类型的- autoPtr,并且这个- autoPtr指向类的一个临时对象(用 C++ 的- new关键字创建 )。这些在宏函数- declareRunTimeSelectionTable中完成。
- 每创建一个派生类,都会调用一次 - addToRunTimeSelectionTable宏函数。这个宏函数会触发一次- hashTable的更新操作。具体地说,宏函数的调用,会往基类里定义的- hashTable插入一组值,这组值的 key 是该派生类的- typeName,value 是一个函数,该函数返回的是指向派生类临时对象的指针。
- 类及其派生类编译成库,在编译过程中,会逐步往 - hashTable增加新元素,直到可选的模型全部添加到其中。
- 在需要调用这些类的地方,只需要定义基类的 - autoPtr,并用基类中定义的- New函数来初始化,即- autoPtr<AlgorithmBase> algorithmPtr = AlgorithmBase::New(algorithmName);。这样,- New函数就能根据调用的时候所提供的参数(即- hashTable的 key),来从- hashTable中选择对应的派生类(即- hashTable的 value)。
经过以上四步,就实现了 RTS 机制。