《OpenFOAM 源码深度剖析》系列-第 4 章 运行期选择机制(RTS):OpenFOAM 的"插件式"对象工厂

第 4 章 运行期选择机制(RTS):OpenFOAM 的"插件式"对象工厂

OpenFOAM 里充斥着这样的配置------turbulenceProperties 写 RAS { RASModel kOmegaSST; },fvSolution 写 p { solver PCG; preconditioner DIC; },0/p 写 type fixedValue;。这些字符串如何在运行期变成一个真正的 C++ 对象?答案就是本章的主角:运行期选择表(Run-Time Selection Table,RTS)。它是 OpenFOAM 最精妙、也最容易被初学者忽略的机制------理解它,就理解了 OpenFOAM 为什么"可插拔"。

4.1 问题的本质

考虑湍流模型:k-ε、k-ω SST、Spalart-Allmaras、Smagorinsky......几十个类,都在编译期存在。但某个具体算例用哪个,只有在运行期读 turbulenceProperties 字典 时才知道。这是经典的抽象工厂模式问题。

C++ 的常规解法(虚函数 + 手写 if/else 字符串映射)在这里不够用,原因有三:

  1. 模型分布在多个库/文件里 ,不可能在工厂函数里集中 #include 所有派生类;
  2. 要支持运行期动态加载 (dlopen 一个额外的 .so 就多一批模型);
  3. 模式要能复用------边界条件、求解器、格式、物性都想要同一套机制。

OpenFOAM 的解法是:每个派生类在"静态初始化期"把自己注册进基类的一张全局表;基类通过一个 New(dict, name) 函数按名字查表构造。 全部由三个宏完成:declare / define / add。

4.2 三个宏的分工

宏 写在 作用
declareRunTimeSelectionTable 基类头文件(类内) 声明构造表类型、单例指针、查找函数、add 助手类
defineRunTimeSelectionTable 基类源文件 为上述声明提供存储与实现的实例化
addToRunTimeSelectionTable 派生类源文件(全局作用域) 用一个静态对象把自己注册进表

以及模板版本:declareRunTimeSelectionTable 的 declareRunTimeNewSelectionTable、defineTemplateRunTimeSelectionTable、addTemplateToRunTimeSelectionTable / addTemplatedToRunTimeSelectionTable 等,用于模板基类。

4.3 declareRunTimeSelectionTableBase:宏展开后的真相

src/OpenFOAM/db/runTimeSelection/construction/runTimeSelectionTables.H 第 54--88 行是最核心的公共基础设施。它接收三个参数:returnType(返回的智能指针包装类型)、prefix(表名前缀,通常等于参数名如 patch/dictionary)、argList(构造器参数列表)。逐条展开:

cpp 复制代码
#define declareRunTimeSelectionTableBase(returnType,prefix,argList)            \
                                                                               \
    /* Function pointer type: construct from argList */                        \
    typedef returnType (*prefix##Ptr)argList;                                  \
                                                                               \
    /* Function table type: construct from argList */                          \
    typedef ::Foam::HashTable                                                  \
    <                                                                          \
        prefix##Ptr,                                                           \
        ::Foam::word,                                                          \
        ::Foam::Hash<::Foam::word>                                             \
    > prefix##TableType;                                                       \
                                                                               \
    typedef ::Foam::HashTable                                                  \
    <                                                                          \
        std::pair<::Foam::word,int>,                                           \
        ::Foam::word,                                                          \
        ::Foam::Hash<::Foam::word>                                             \
    > prefix##CompatTableType;                                                 \
                                                                               \
    static prefix##TableType* prefix##TablePtr_;                               \
    static std::unique_ptr<prefix##CompatTableType> prefix##CompatTablePtr_;   \
    static prefix##CompatTableType& prefix##CompatTable();                     \
    static void prefix##TablePtr_construct(bool load);                         \
    static prefix##Ptr prefix##Table(const ::Foam::word& k)

解读:

  1. <prefix>Ptr 是函数指针类型 ------"从一个 argList 构造出 returnType"。例如对于边界条件 patch 表,patchPtr 就是 tmp<fvPatchField<Type>> (*)(const fvPatch&, const DimensionedField<Type, volMesh>&)。
  2. <prefix>TableType 是 HashTable<函数指针, word>------一个从"字符串名字"到"构造函数指针"的映射。这就是那张全局注册表。
  3. <prefix>CompatTableType 是 HashTable<pair<word,int>, word> ------从"废弃名"到"新名 + 版本号"的别名表。用于向后兼容:老字典写 RNGkEpsilon,可以自动映射到新名,并给出弃用告警。
  4. <prefix>TablePtr_:表的单例指针(裸指针,静态存储)。
  5. <prefix>CompatTablePtr_ :别名表的单例指针(unique_ptr,自动清理,见源码注释 uses automatic cleanup for compat table (issue #2314))。
  6. <prefix>Table(k) :查找函数,返回函数指针,找不到返回 nullptr。

4.4 defineRunTimeSelectionTableBase:定义存储与查找

第 93--162 行把上面的声明落地成定义。其中几个设计细节非常值得品味。

4.4.1 单例的"懒惰构造 + 显式析构"

cpp 复制代码
    Tspecialize void prefix##TablePtr_construct(bool load)                     \
    {                                                                          \
        static bool constructed = false;                                       \
        if (load)                                                              \
        {                                                                      \
            if (!constructed)                                                  \
            {                                                                  \
                prefix##TablePtr_ = new prefix##TableType;                     \
                constructed = true;                                            \
            }                                                                  \
        }                                                                      \
        else if (prefix##TablePtr_)                                            \
        {                                                                      \
            delete prefix##TablePtr_;                                          \
            prefix##TablePtr_ = nullptr;                                       \
        }                                                                      \
    }

为什么要这样设计?因为 C++ 的静态初始化顺序不确定 。表本身是一个全局单例,而注册它的派生类也都是全局对象;谁先谁后无法保证。OpenFOAM 的解决办法是:创建表时用 constructed 静态布尔保证幂等 ,销毁时机则由每个 add 助手的析构函数在最后一个注册者析构时触发(见 4.5)。这样无论静态初始化顺序如何,表都能正确建立与回收。

4.4.2 查找:先精确名,再兼容名

cpp 复制代码
    Tspecialize prefix##Ptr prefix##Table(const ::Foam::word& k)               \
    {                                                                          \
        if (prefix##TablePtr_)                                                 \
        {                                                                      \
            const auto& tbl = *prefix##TablePtr_;                              \
            auto iter = tbl.cfind(k);                                          \
            if (!iter.good() && prefix##CompatTablePtr_)                       \
            {                                                                  \
                const auto altIter = prefix##CompatTablePtr_->cfind(k);        \
                if (altIter.good())                                            \
                {                                                              \
                    const auto& alt = altIter.val();  /* <word,int> */         \
                    iter = tbl.cfind(alt.first);                               \
                    if (::Foam::error::warnAboutAge(alt.second))               \
                    {                                                          \
                        std::cerr                                              \
                            << "Using [v" << alt.second << "] '" << k          \
                            << "' instead of '" << alt.first                   \
                            << "' in selection table: "                        \
                            << #baseType << '\n' << std::endl;                 \
                        ::Foam::error::warnAboutAge("lookup", alt.second);     \
                    }                                                          \
                }                                                              \
            }                                                                  \
            if (iter.good())                                                   \
            {                                                                  \
                return iter.val();                                             \
            }                                                                  \
        }                                                                      \
        return nullptr;                                                        \
    }

查找逻辑:先在主表按名查;查不到再查"兼容表",若命中则用新名再查一次,并按年龄给出弃用提示。这是一套在运行期生效的名字迁移机制------你不用改老算例,代码替你改写并提醒。

4.5 add...ConstructorToTable:自注册的魔法

declareRunTimeSelectionTable(第 173--229 行)在展开公共基础设施后,还定义了一个模板助手类:

cpp 复制代码
    /* Helper to add constructor from argList to table */
    template<class baseType##Type>
    struct add##argNames##ConstructorToTable
    {
        static ptrWrapper<baseType> New argList
        {
            return ptrWrapper<baseType>(new baseType##Type parList);
        }

        explicit add##argNames##ConstructorToTable
        (
            const ::Foam::word& k = baseType##Type::typeName     // 默认键 = 派生类的 typeName
        )
        {
            argNames##ConstructorTablePtr_construct(true);
            if (!argNames##ConstructorTablePtr_->insert(k, New))
            {
                std::cerr
                    << "Duplicate entry " << k << " in runtime table "
                    << #baseType << std::endl;
                ::Foam::error::safePrintStack(std::cerr);
            }
        }

        ~add##argNames##ConstructorToTable()
        {
            argNames##ConstructorTablePtr_construct(false);
        }
        // 禁止拷贝/赋值
    };

这个模板类只有一件工作:在它的构造函数里,把"构造器 New"以键 k 插入全局表。

  • New 是一个静态函数:new 派生类(...) 后用 tmp/autoPtr 包起来。也就是说,表里存的"工厂",本质就是 new。
  • 默认键 k = 派生类::typeName------这正是 TypeName("kEpsilon") 宏提供的静态字符串。
  • 构造时 prefix##TablePtr_construct(true) 确保表已建好;插入失败(重名)会打印"Duplicate entry"并栈回溯。
  • 析构时 prefix##TablePtr_construct(false) 递减/销毁表。

4.5.1 注册的落地:一个文件作用域的静态对象

addToRunTimeSelectionTable.H(第 42--47 行)把上面这个模板类实例化成一个文件作用域的静态对象:

cpp 复制代码
#define addToRunTimeSelectionTable\
(baseType,thisType,argNames)                                                   \
                                                                               \
    /* Add thisType factory method to the table */                             \
    baseType::add##argNames##ConstructorToTable<thisType>                      \
        add##thisType##argNames##ConstructorTo##baseType##Table_

展开后就是:

cpp 复制代码
// 在派生类的 .C 文件里,namespace Foam 内(或全局作用域)
fvPatchField<Type>::adddictionaryConstructorToTable<myFvPatchField<Type>>
    addmyFvPatchFielddictionaryConstructorTofvPatchFieldTable_;

这个对象在程序加载(静态初始化)时 自动运行构造函数,从而完成注册。这就是"静态自注册 "(self-registration)惯用法。没有任何显式的 registerAll() 调用;只要你链接了包含该 .C 的库,对应的模型就自动可用。

4.6 等价变体:命名注册、别名、模板

4.6.1 用自定义名字注册

addNamedToRunTimeSelectionTable(第 60--66 行)允许用 lookupName 而非 typeName 作键:

cpp 复制代码
#define addNamedToRunTimeSelectionTable\
(baseType,thisType,argNames,lookupName)                                        \
    baseType::add##argNames##ConstructorToTable<thisType>                      \
        add##thisType##argNames##ConstructorTo##\
        baseType##Table_##lookupName##_(#lookupName)

这样一个类可以在多个名字下被查找(例如 RNGkEpsilon 与 kEpsilonRNG)。

4.6.2 别名与弃用迁移

addAliasToRunTimeSelectionTable(第 80--86 行)与配套的 addAlias##argNames##ConstructorToTable(第 181--193 行):

cpp 复制代码
    struct addAlias##argNames##ConstructorToTable                              \
    {                                                                          \
        explicit addAlias##argNames##ConstructorToTable                        \
        (const ::Foam::word& k, const ::Foam::word& alias, const int ver)       \
        {                                                                      \
            argNames##ConstructorCompatTable()                                 \
                .set(alias, std::pair<::Foam::word,int>(k,ver));               \
        }                                                                      \
    };

它不注册构造器 ,只在"兼容表"里把 alias 映射到 (k, ver)。配合 4.4.2 的查找逻辑,老名字 alias 会被自动解析为新名字 k,并按 ver 给出弃用告警。这就是 OpenFOAM 在版本迭代中让旧算例继续运行的"软着陆"机制。

4.6.3 "New 表"变体

declareRunTimeNewSelectionTable(第 271--364 行)用于"派生类本身也是一个工厂"的场景------表里存的不再是 new,而是调用派生类的 New(...):

cpp 复制代码
        static ptrWrapper<baseType> New##baseType argList
        {
            return ptrWrapper<baseType>(baseType##Type::New parList.ptr());
        }

4.6.4 模板基类的三套宏

C++ 里模板基类无法用普通 declare/define 宏(虚成员与静态成员的定义语法不同),因此专门有:

  • defineTemplateRunTimeSelectionTable(baseType, argNames):当 baseType 是 typedef(不需模板参数)时用;
  • defineTemplatedRunTimeSelectionTable(baseType, argNames, Targ):当 baseType 需要模板参数时用;
  • addTemplateToRunTimeSelectionTable / addTemplatedToRunTimeSelectionTable:对应的注册宏。

4.7 三个真实案例

4.7.1 边界条件:fvPatchField 的三张表

src/finiteVolume/fields/fvPatchFields/fvPatchField/fvPatchField.H(第 352--389 行)声明了三张选择表:

cpp 复制代码
declareRunTimeSelectionTable
(
    tmp, fvPatchField, patch,
    (const fvPatch& p, const DimensionedField<Type, volMesh>& iF),
    (p, iF)
);

declareRunTimeSelectionTable
(
    tmp, fvPatchField, patchMapper,
    (const fvPatchField<Type>& ptf, const fvPatch& p,
     const DimensionedField<Type, volMesh>& iF, const fvPatchFieldMapper& m),
    (dynamic_cast<const fvPatchFieldType&>(ptf), p, iF, m)
);

declareRunTimeSelectionTable
(
    tmp, fvPatchField, dictionary,
    (const fvPatch& p, const DimensionedField<Type, volMesh>& iF,
     const dictionary& dict),
    (p, iF, dict)
);

为什么是三张?因为边界条件有三套构造来源:

表名 构造来源 用途
patch 从 patch + 内部场 程序内部生成(如 zeroGradient 默认)
patchMapper 从另一个 patchField + mapper 网格映射/拓扑变化时重建
dictionary 从 patch + 内部场 + 字典 从 0/p 的 type ...; 读入

每个具体边界条件用 makePatchTypeField 宏把三种构造一次性注册。以 fixedFluxPressureFvPatchScalarField 为例(.cxx 第 166--173 行):

cpp 复制代码
namespace Foam
{
    makePatchTypeField
    (
        fvPatchScalarField,
        fixedFluxPressureFvPatchScalarField
    );
}

makePatchTypeField 内部展开成三条 addToRunTimeSelectionTable(分别对应 patch/patchMapper/dictionary 表),加上 TypeName 与 typedefs。于是只要用户写 type fixedFluxPressure;,fvPatchField::New 便能按 dictionary 表构造它。

4.7.2 湍流模型:模板 RTS 与"多态家族"

湍流模型的 RTS 由 makeTurbulenceModel.H 的宏家族生成。makeBaseTurbulenceModel(第 65--117 行)一次性为三种模型族建立表与注册:

cpp 复制代码
    namespace Foam
    {
        /* Turbulence selection table */
        defineTemplateRunTimeSelectionTable(Transport##baseModel, dictionary);

        /* Laminar models */
        defineNamedTemplateTypeNameAndDebug(laminar##Transport##BaseModel, 0);
        defineTemplateRunTimeSelectionTable(laminar##Transport##BaseModel, dictionary);
        addToRunTimeSelectionTable(Transport##baseModel, laminar##Transport##BaseModel, dictionary);

        /* RAS models */
        ...
        /* LES model types */
        ...
    }

而单个模型(如 kEpsilon)用 makeTurbulenceModel(第 162--176 行)注册:

cpp 复制代码
#define makeTurbulenceModel(BaseModel, SType, Type)                            \
    namespace Foam                                                             \
    {                                                                          \
        namespace SType##Models                                                \
        {                                                                      \
            defineTypeNameAndDebug(Type, 0);                                   \
            addToRunTimeSelectionTable(SType##BaseModel, Type, dictionary);    \
        }                                                                      \
    }

这就是为什么 turbulentFluidThermoModels.C(第 74--116 行)里能一行一个地把 kEpsilon、kOmega、kOmegaSST、SpalartAllmaras、realizableKE、GEKO......全部登记进 RASModel 表:

cpp 复制代码
makeRASModel(SpalartAllmaras);
makeRASModel(kEpsilon);
makeRASModel(RNGkEpsilon);
makeRASModel(realizableKE);
makeRASModel(kOmega);
makeRASModel(kOmegaSST);
...

kEpsilon.H 第 165 行的 TypeName("kEpsilon") 提供了键名,new 出的对象类型则是模板实例族(如 kEpsilon<EddyDiffusivity<fluidThermoModel>>)。

4.7.3 线性求解器:对称/非对称两张表

LduMatrix.H(第 184--228 行)为求解器声明了 symMatrix 与 asymMatrix 两张表------因为对称矩阵(如 laplacian)可用 CG,非对称(如 div)必须用 BiCGStab:

cpp 复制代码
declareRunTimeSelectionTable(autoPtr, solver, symMatrix,
    (const word& fieldName, const lduMatrix& matrix,
     const FieldField<Field, scalar>& interfaceBouCoeffs,
     const FieldField<Field, scalar>& interfaceIntCoeffs,
     const lduInterfaceFieldPtrsList& interfaces,
     const dictionary& solverControls),
    (fieldName, matrix, interfaceBouCoeffs, interfaceIntCoeffs,
     interfaces, solverControls));

declareRunTimeSelectionTable(autoPtr, solver, asymMatrix, ...);

于是 fvSolution 里写 solver PCG; / solver PBiCGStab; / solver GAMG;,就会分别落到对应的表去构造。

4.8 为什么这套机制如此重要

把 RTS 放回整个框架看,它带来了四个决定性的能力:

  1. 可插拔(Pluggable) :添加一个湍流模型/边界条件/格式,只需新建 .H/.C 并 addToRunTimeSelectionTable,不必改动任何基类或工厂代码。
  2. 跨库组装 :模型可以分散在不同 .so 中,靠链接与静态初始化自动汇入同一张表。Time 里甚至有"动态库表",支持运行期 dlopen 额外库(controlDict 的 libs 条目)。
  3. 字典驱动、用户友好:用户只写字符串名,程序自己找到实现,并负责弃用告警。
  4. 统一模式 :从边界条件到湍流模型到线性求解器到函数对象,全都遵循同一套 declare/define/add 心智模型。学会一次,处处适用。

4.9 使用 RTS 的注意事项(工程经验)

  • 键名必须唯一 :重名会在 insert 时打印 Duplicate entry 并栈回溯。多库同时定义同名模型是常见坑。
  • 静态初始化顺序 :正是为此,OpenFOAM 才用 constructed + 引用计数式的析构(4.4.1)。自定义库时不要绕过这套机制自己管理全局表。
  • 模板类型名 :模板模型的键名靠 defineNamedTemplateTypeNameAndDebug 生成,注意 TypeName 与 New 的对齐。
  • 别名迁移 :升级版本时给旧名加 addAliasToRunTimeSelectionTable,可以让老算例无痛过渡。
  • 调试 :FOAM_DEBUG/DebugInfo 与 error::warnAboutAge 是排查"为什么没找到某个模型"的入口。

本章要点回顾

  • RTS = 抽象工厂 + 字符串注册表 + 静态自注册 ,由 declare/define/add 三宏实现。
  • declareRunTimeSelectionTableBase 展开出 <prefix>Ptr(函数指针)、<prefix>TableType(HashTable<Ptr,word>)、单例指针与查找函数。
  • defineRunTimeSelectionTableBase 提供存储与查找实现,用 constructed 布尔解决静态初始化顺序 问题,用兼容表实现旧名迁移。
  • addToRunTimeSelectionTable 在派生类源文件生成一个文件作用域静态对象,其构造即注册。
  • 变体覆盖:命名注册(addNamed...)、别名迁移(addAlias...)、"New 表"、模板基类三套宏。
  • 真实案例:fvPatchField 三张表、湍流模型模板 RTS 家族、lduMatrix::solver 的 sym/asym 双表。
相关推荐
鶴哥只手遮天1 小时前
《OpenFOAM 源码深度剖析》系列-第 3 章 字典系统、IO 体系与对象注册表
算法·开源
鶴哥只手遮天1 小时前
《OpenFOAM 源码深度剖析》系列-第 2 章 wmake:OpenFOAM 自研构建系统与编译配置体系
算法·开源
用户81849100742161 小时前
我把工位上的“AI牛马”养成了:一个技术人的 AiPy 实战手记
算法
Tisfy1 小时前
LeetCode 3727.最大交替平方和:排序 (双指针可选)
算法·leetcode·题解·贪心·数组·排序
鶴哥只手遮天1 小时前
《OpenFOAM 源码深度剖析》系列-第 9 章 有限体积离散框架:`fvMatrix`、`fvSchemes` 与 `fvSolution`
算法·开源
鶴哥只手遮天1 小时前
《OpenFOAM 源码深度剖析》系列-第 7 章 网格与拓扑体系:`polyMesh`、`primitiveMesh` 与 patch
算法·开源
鶴哥只手遮天1 小时前
《OpenFOAM 源码深度剖析》系列-第 5 章 数值基元、维度系统与 `Field` 体系
算法·开源
鶴哥只手遮天1 小时前
《OpenFOAM 源码深度剖析》系列-第 10 章 离散算子与插值格式:`grad` / `div` / `laplacian` / `ddt` 的实现
算法·开源
CHEEVEN_QY1 小时前
热仿真在液冷板设计中的应用价值与局限性客观评估
人工智能·算法