运行时选择(run-time selection,RTS)机制是 OpenFOAM 的一大特点。RTS 做的工作其实很简单:通过运行时(从字典读入)的不同关键字构造不同对象,这些构造的对象类都派生自同一父类,具有一组相同的接口。RTS 的实现涉及到工厂模式(factory pattern)、哈希表(hash table)和宏(macro),本文将从这三个方面介绍 RTS 的实现原理。

工厂模式

工厂模式是一种设计模式(design pattern),是用来提高代码可维护性的一种技巧。常见的工厂模式有三类:简单工厂(simple factory)、工厂方法(factory method)和抽象工厂(abstract factory)。OpenFOAM 用的是简单工厂,即在工厂类中提供一个静态函数用来构造所需对象并返回,这个静态函数一般叫 New ,OpenFOAM 称之为 Selector。所有涉及到 RTS 的工厂类(父类)中都可以找到这个函数。

传统的简单工厂模式有个比较严重的缺陷:在构造所需对象的时候,所有的可构造对象只能是事先考虑到的,如果需要添加对象类,则需要修改工厂。这样不利于代码的维护。OpenFOAM 使用哈希表构造了一个 constructor table,将可构造的对象类存储在这个哈希表中,从而避免了新增具体产品类时对工厂类的修改。

以时间离散格式类 ddtScheme 为例说明,相关代码:

src/finiteVolume/finiteVolume/ddtSchemes/ddtScheme/ddtScheme.H
src/finiteVolume/finiteVolume/ddtSchemes/ddtScheme/ddtScheme.C
src/finiteVolume/finiteVolume/ddtSchemes/ddtScheme/ddtSchemes.H

ddtScheme::New 的实现如下:

 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
template<class Type>
tmp<ddtScheme<Type> > ddtScheme<Type>::New
(
    const fvMesh& mesh,
    Istream& schemeData
)
{
    if (fv::debug)
    {
        Info<< "ddtScheme<Type>::New(const fvMesh&, Istream&) : "
               "constructing ddtScheme<Type>"
            << endl;
    }

    if (schemeData.eof())
    {
        FatalIOErrorIn
        (
            "ddtScheme<Type>::New(const fvMesh&, Istream&)",
            schemeData
        )   << "Ddt scheme not specified" << endl << endl
            << "Valid ddt schemes are :" << endl
            << IstreamConstructorTablePtr_->sortedToc()
            << exit(FatalIOError);
    }

    const word schemeName(schemeData);

    typename IstreamConstructorTable::iterator cstrIter =
        IstreamConstructorTablePtr_->find(schemeName);

    if (cstrIter == IstreamConstructorTablePtr_->end())
    {
        FatalIOErrorIn
        (
            "ddtScheme<Type>::New(const fvMesh&, Istream&)",
            schemeData
        )   << "Unknown ddt scheme " << schemeName << nl << nl
            << "Valid ddt schemes are :" << endl
            << IstreamConstructorTablePtr_->sortedToc()
            << exit(FatalIOError);
    }

    return cstrIter()(mesh, schemeData);
}

New 函数从哈希表中通过关键字 schemeName 查找与之对应的具体时间离散格式(如 Euler、backward 等)的构造函数,并返回其所构造的对象。这样就通过简单工厂实现了 RTS 的基本功能。

哈希表

哈希表的本质是一张 key -> value 键值对映射表,表中的 key 和 value 一一对应,可以通过 key 访问 value。OpenFOAM 使用哈希表用来维护工厂类可构造的具体产品类,这个哈希表使用类名作为 key,使用一系列静态函数的指针作为 value 。这样就可以通过类名调用其对应的静态函数构造并返回相应的对象。我们先看哈希表声明的相关代码:

 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
template<class Type>
class ddtScheme
:
    public refCount
{
    ...

public:

    ...

    /* Construct from argList function pointer type */
    typedef autoPtr<ddtScheme> (*IstreamConstructorPtr)(const fvMesh& mesh, Istream& schemeData);

    /* Construct from argList function table type */
    typedef HashTable<IstreamConstructorPtr, word, string::hash> IstreamConstructorTable;

    /* Construct from argList function pointer table pointer */
    static IstreamConstructorTable* IstreamConstructorTablePtr_; 

    /* Table constructor called from the table add function */
    static void constructIstreamConstructorTables();

    /* Table destructor called from the table add function destructor */
    static void destroyIstreamConstructorTables();

    /* Class to add constructor from argList to table */
    template<class ddtSchemeType>
    class addIstreamConstructorToTable
    {
    public:

        static autoPtr<ddtScheme> New(const fvMesh& mesh, Istream& schemeData)
        {
            return autoPtr<ddtScheme>(new ddtSchemeType(mesh, schemeData));
        }

        addIstreamConstructorToTable
        (
            const word& lookup = ddtSchemeType::typeName;
        )
        {
            constructIstreamConstructorTables();
            if (!IstreamConstructorTablePtr_->insert(lookup, New))
            {
                std::cerr<< "Duplicate entry " << lookup
                    << " in runtime selection table " << "ddtScheme"
                    << std::endl;
                error::safePrintStack(std::cerr);
            }
        }

        ~addIstreamConstructorToTable()
        {
            destroyIstreamConstructorTables();
        }
    };

    ...

这里定义了一个哈希表 IstreamConstructorTablePtr_ 用来存储类名-函数指针的键值对 。同时声明了一个 addIstreamConstructorToTable 类模板,类模板中声明了一个静态函数 New,该函数将返回新构造的具体时间离散格式(如 Euler、backward等)实例。类的构造函数和析构函数分别调用了 constructIstreamConstructorTablesdestroyIstreamConstructorTables 这两个函数,我们看其实现:

 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
    /* Define the constructor function table */
    template<>
    ddtScheme<scalar>::IstreamConstructorTable*
        ddtScheme<scalar>::IstreamConstructorTablePtr_ = NULL;

    /* Table constructor called from the table add function */
    template<>
    void ddtScheme<scalar>::constructIstreamConstructorTables()
    {
        static bool constructed = false;
        if (!constructed)
        {
            constructed = true;
            ddtScheme<scalar>::IstreamConstructorTablePtr_
                = new ddtScheme<scalar>::IstreamConstructorTable;
        }
    }

    /* Table destructor called from the table add function destructor */
    template<>
    ddtScheme<scalar>::destroyIstreamConstructorTables()
    {
        if (ddtScheme<scalar>::IstreamConstructorTablePtr_)
        {
            delete ddtScheme<scalar>::IstreamConstructorTablePtr_;
            ddtScheme<scalar>::IstreamConstructorTablePtr_ = NULL;
        }
    }

    ...

这里用到了 C++ 的模板特化(template specialization),将模板参数特化为 scalar、vector 等绝对类型。上面代码列举了模板参数特化为 scalar 类型的实现,constructIstreamConstructorTables 函数的作用是如果不存在则构造一个哈希表,destroyIstreamConstructorTables 函数的作用是如果存在则销毁哈希表。

有了哈希表,接下来需要向哈希表中添加记录,这些添加操作对应的代码在各具体产品类(子类)中可以找到。以 Euler 格式为例:

1
2
ddtScheme<scalar>::addIstreamConstructorToTable<EulerDdtScheme<scalar> >
    addEulerScalarIstreamConstructorToTable_;

这里定义了一个 addIstreamConstructorToTable<EulerDdtScheme<scalar>> 类型的变量,其构造函数首先调用 constructIstreamConstructorTables 构造哈希表,再使用 HashTable::insert 方法往表中添加记录,这些记录用类名 ddtSchemeType::typeName 作为 key,用对应类的 New 函数的函数指针作为 value。在所有的子类中添加上述代码,可以把所有子类的记录都添加到哈希表中。

上面提到的工厂模式和哈希表已经实现了 RTS 的所有功能,但是我们看到对于每个 RTS 类型(如gradSchemesdivSchemes 等)都需要增加大量的代码,这些代码结构几乎没有区别。OpenFOAM 采用宏定义来压缩这些代码,相关代码:

src/OpenFOAM/db/runTimeSelection/construction/addToRunTimeSelectionTable.H
src/OpenFOAM/db/runTimeSelection/construction/runTimeSelectionTables.H

比如上面关于哈希表的代码都可以用 declareRunTimeSelectionTabledefineTemplateRunTimeSelectionTable 以及 makeFvDdtTypeScheme 等宏定义展开,具体展开过程这里不再赘述。