本部分介绍了C++和OpenFOAM函数对象(Function Object)的软件设计。 C++和OpenFOAM函数对象之间的差异应该明确,因为它们可能会导致混乱。

12.1.1 C++中的函数对象

详细描述C++中函数对象的实现和用法超出了本书的范围,在文献[2]、[3]和[1]等中有详细描述。本文简要概述了C++函数对象,足以将其与OpenFOAM函数对象区分开来。

正如其名称所示,函数对象是行为类似函数的对象。在 C++编程语言中,当操作符 operator()为其类重载时,其对象的行为就像函数一样。清单74显示了一个非常简化的 C++函数对象示例。重载算术运算符(+、-、*、/)允许类对其对象实现算术操作,尽管实现不限于算术操作。这种 C++语言特性被广泛用于 OpenFOAM 本身的代数场运算。以类似的方式,重载类的 operator()()使其对象具有类似函数的行为。

// list 74
class CallableClass
{
    public:
        void operator()() {}
};
int main(int argc, const char *argv[])
{
    CallableClass c;
    c();
    return 0;
}

函数对象是对象这一事实带来了不同的优点:

  • 函数对象可以存储有关其状态的附加信息。
  • 它被实现为一种类型,这是泛型编程中经常使用的事实。
  • 它的执行将比涉及传递函数指针的代码更快,因为函数对象通常是内联的。

函数对象可以在处理operator()()中的参数时累积信息,并将累积的信息存储为数据成员以供以后使用。

下面的C++函数对象示例显示了如何实现OpenFOAM类,该类在C++ STL函数对象的帮助下选择网格单元,而不是依赖于OpenFOAM数据结构。此类名为fieldCellSet,可在示例代码存储库的ofBookFunctionObjects文件夹中找到。

OpenFOAM中的网格选择通常是拓扑的,例如选择中心位于球体内的网格的标签。此示例中实现的fieldCellSet类使用volScalarField的场值从网格中收集像元。如果场值满足特定条件,则网格选择器会将特定的网格标签添加到一组标签中。

遵循SRP,通过将单元选择函数定义为根据选择标准参数化的函数模板,将类fieldCellSet与单元选择标准解耦。在下面的两个代码片段中,此模板参数属于泛型类型Collector,参数col存储此类型的对象。因此,fieldCellSet使用模板参数来描述collectCells中的实际选择标准。

//- Edit
template<typename Collector> 
void collectCells(const volScalarField&amp; field, Collector col);

fieldCellSet能够与任何实现了operator()()的Collector函数对象一起工作,接受标量值并返回布尔值,这就是它的结果。Collector模板参数的概念可以在collectCells成员函数模板的实现中检查,该模板在文件fieldCellSetTemplates.C中定义:

template<typename Collector> 
void fieldCellSet::collectCells
(
    const volScalarField&amp; field,
    Collector col
)
{
    forAll(field, I)
    {
        if (col(field[I]))
        {
            insert(I);
        }
    }
}

如上面的代码片段所示,collectCells成员函数只是对所有场值进行迭代,并期望Collector返回一个布尔变量作为对场值field[I]进行操作的结果。这指定了收集器模板参数概念:

  • callable:函数对象是自然选择的
  • unary:它必须至少允许一个函数参数
  • predicate:它返回一个布尔变量

总而言之,小单元格收集器类fieldCellSet有一个成员函数模板,该模板将volScalarField和函数对象col作为参数。它使用函数对象来检查是否应根据字段的值将单元格添加到单元格集中。但是,如何实现这种比较并不重要,只要实现了col的operator()并返回布尔值即可。

这种简单的实现允许将任何函数对象传递给collectCells成员函数模板。在下面的示例中,使用了STL的函数对象。单个fieldCellSet类的测试应用程序的实现可以在示例代码存储库中找到,位于applications/test子目录下,名为testFieldCellSet。testFieldCellSet中与C++中的函数对象相关的部分是一行代码:

fcs.collectCells(field, std::bind1st(std::equal_to<scalar> (), 1));

collectCells类的对象fcs电泳collectCells方法。

本例中使用的函数对象是equal_to STL函数对象模板。这个通用函数对象只检查类型T的两个值是否相同。由于它使用泛型编程的类型提升方面,因此只能应用于定义了相等运算符==的类型的实例。但是,equal_to函数对象需要两个参数进行比较。剩下的问题是如何在collectCells成员函数模板中为字段值调用它。对于这个简单的例子,答案相当简单:equal_to函数对象的第一个参数已绑定到值1。这意味着testFieldCellSet应用程序应创建一个由场值等于1的所有网格单元组成的单元集合。

面向对象的设计允许fieldCellSet类委托网格标签的存储,以及委托在每个新时间步长执行的输出操作。这是通过多重继承实现的:

class fieldCellSet
:
    public labelHashSet,
    public regIOobject
    {

在上面的代码片段中,labelHashSet是OpenFOAM类模板HashSet的实例化,它使用Foam::label作为关键值。

从regIOobject继承允许类在对象注册表中注册,并在调用时间增量运算符(Time::operator++())时自动写入磁盘。写入以这样一种方式执行,即输出文件包含OpenFOAM稍后重新读取数据所需的必要OpenFOAM文件头信息。

testFieldCellSet应用程序可以在第11章的示例中进行测试,即

/ofprimer/cases/chapter11/falling-droplet-2D

通过以下方式调用:

?>  blockMesh
?>  setAlphaField
?>  testFieldCellSet -field alpha.water

生成的网格集存储在0目录中,要使用paraView应用程序对其进行可视化,必须使用foamToVTK将其转换为VTK格式。在此之前,必须将网格集从0目录复制到constant/polyMesh/sets:

?>  mkdir -p constant/polyMesh/sets
?>  cp 0/fieldCellSet !$

完成此操作后,网格集将转换为

?>  foamToVTK -cellSet fieldCellSet

这将在下落液滴2D案例下生成VTK目录。图12.1显示了初始时间步长的下落液滴单元集的alpha1场。在paraView应用程序中,可以轻松地完成基于区域的网格集的计算和可视化,但关键是要了解C++中的函数对象,以及在编写OpenFOAM代码时如何轻松使用这些对象。C++中的函数对象与扩展函数非常相似。OpenFOAM中的函数对象与C++函数对象具有相似的功能,但它们的设计不同。

图12.1 的场alpha1其包括初始时间步长中的上升气泡

12.1.2 OpenFOAM中的函数对象

OpenFOAM中的函数对象具有与标准C++函数对象不同的类接口。它们不依赖于重载operator(),而是使用functionObject抽象类定义的类接口,如清单75所示。因此OpenFOAM函数对象的类层次结构在结构上类似于边界条件(第10章)和传输模型(第11章)所使用的层次结构。抽象基类(functionObject)规定了OpenFOAM中所有函数对象的接口。与重载调用运算符的标准C++函数对象相比,OpenFOAM函数对象使用各种虚函数,使得它们可以在模拟循环的不同步骤中被调用。

// List75
// Member Functions
//- Name
virtual const word&amp; name() const;
//- Called at the start of the time-loop
virtual bool start() = 0;
//- Called at each ++ or += of the time-loop.
// forceWrite overrides the outputControl behaviour.
virtual bool execute(const bool forceWrite) = 0;
//- Called when Time::run() determines that
// the time-loop exits.
// By default it simply calls execute().
virtual bool end();
//- Called when time was set at the end of
// the Time::operator++
virtual bool timeSet();
//- Read and set the function object if
// its data have changed.
virtual bool read(const dictionary&amp;) = 0;
//- Update for changes of mesh
virtual void updateMesh(const mapPolyMesh&amp; mpm) = 0;
//- Update for changes of mesh
virtual void movePoints(const polyMesh&amp; mesh) = 0;

functionObject成员函数的执行与模拟时间的更改或网格的更改有关,如清单75所示。OpenFOAM中的模拟由Time类控制,functionObject的大多数成员函数都与模拟时间的变化有关。因此,Time类负责根据事件调用functionObject成员函数。使用对模拟时间的常量访问,从网格类polyMesh中调用与网格运动(movePoints)和场映射(updateMesh)相关的两个成员函数。

作为这些要求的结果,Time类将为每个特定模拟加载的所有函数对象组合到函数对象列表(functionObjectList)中,如图12.2所示。

图12.2 Time类中函数对象的组成

functionObjectList实现functionObject的接口,并将成员函数调用委托给复合函数对象。当模拟开始并且Time类的runTime对象初始化时,读取模拟控制字典controlDict。functionObjectList的构造函数读取controlDict并解析函数子字典中的条目。函数子字典中的每个条目定义单个函数对象的参数,然后将这些参数传递给函数对象选择器。选择器(functionObject::New)实现了OOP中已知的“工厂模式”,并在运行时使用字典参数类型初始化functionObject抽象类的具体模型。最后,将选定的函数对象追加到函数对象列表中。该机制还依赖于OpenFOAM中的RTS机制,允许用户针对不同的仿真用例选择和实例化不同的功能对象。

由于函数对象在运行时被初始化,并且依赖于动态多态性(虚拟函数),因此实现主功能的成员函数在解析调用哪个虚拟成员函数时会产生开销(动态调度)。然而,由OpenFOAM函数对象执行的计算所花费的计算时间比动态调度多几个数量级,因此使用动态调度的开销可以安全地忽略。

OpenFOAM中的函数对象也是在模拟过程中执行类似函数操作的对象,因此该属性使其名称合理。functionObject的类声明用作描述进一步差异的另一个示例:

// Private Member Functions
//- Disallow default bitwise copy construct
functionObject(const functionObject&amp;);
//- Disallow default bitwise assignment
void operator=(const functionObject&amp;);

与C++函数对象相反,OpenFOAM中的函数对象禁止赋值和复制构造,因此无法将它们作为值参数传递。禁止的赋值和复制构造对OpenFOAM函数对象没有负面影响:OpenFOAM中函数对象的中心使用点是Time类中的私有属性functionObjectList,因此不需要在列表中手动实例化它们或按值传递它们。

OpenFOAM函数对象的设计及其与Time类的交互已经介绍完毕,剩下的一点就是新函数对象的编程。基本上,任何继承自functionObject的类,实现其接口,并且其类型入口以及必要的参数列在controlDict模拟控制目录中,都将被OpenFOAM解释为函数对象。模拟中初始化函数对象的过程由Time类执行并自动发生,前提是添加了函数对象配置子字典,并且在system/controlDict文件中加载了实现函数对象的动态链接库。然而,在开发新的函数对象之前,建议检查所需的功能是否已经在OpenFOAM或社区贡献中可用。