沿着模板类使用非虚接口向下转换

Downcasting using a non-virtual interface along a template class

本文关键字:接口 转换      更新时间:2023-10-16

我正在实现一个有限元代码。

<标题>

问题描述在有限元方法中,我们需要一个积分器和一个插值器。积分器是对几何对象(如四边形、三角形等)进行数值积分的对象。积分器在几何对象内放置多个积分点或横坐标,然后使用插值器来近似这些积分点处的函数值。

例如,一个四边形对象可以使用一个包含4个积分点的积分器。

* ------------- *
|               |
|    @     @    |
|               |
|               |
|    @     @    |
|               |
* ------------- *

,其中@表示集成点的位置。插值器通过使用角节点处的值(用*表示)来近似这些积分点处的函数值。你可以把它想象成@的每个值是所有*的值的一种平均值。

多态性概述为方便起见,下面的图表显示了问题中使用的不同类之间的联系:

Interpolator
|
| is a base for:
v
Interpolator_Template
|
| is a base for:
|
------------------------------------------
|                              |         |
|                              |    More shapes
V                              V
Interpolator_Quad         Interpolator_Tria
|    |                         |
|   ... More QUADs         More TRIAs
v
Interpolator_Quad_04

Integrator
|
| has member variable
v
vector<Abscissa*> abscissa
|
| Abscissa is a base class for:
|
--------------------------------
|               |              |
|          More shapes         |
V                              V
Abscissa_Quad               Abscissar_Tria

每个几何形状都有不同的坐标系,所以我的积分器和横坐标是这样的:

Integrator.hpp

class Integrator {
public:
void
integrate();
private:
/**
* An interpolator class.
*/
Interpolator * _interpolator;
/**
* List of abscissae (for the quadrilateral shown above, _abscissae.size() == 4).
*/
std::vector< Abscissa * > _abscissae;
};

所有自然坐标横坐标的基类。

Abscissa.hpp

class Abscissa {
};

一个四边形横坐标作用于ξ和η自然坐标。

Abscissa_Quad.hpp

class Abscissa_Quad final : public Abscissa {
public:
const double xi;
const double eta;
};

一个三角形的横坐标作用于ζ1、ζ2和ζ3自然坐标。

Abscissa_Tria.hpp

class Abscissa_Tria final : public Abscissa {
public:
const double zeta_1;
const double zeta_2;
const double zeta_3;
};

积分器的实现将像这样集成:

Integrator.cpp

void
Integrator::integrate()
{
for ( Abscissa * abscissa : _abscissae ) {
_intepolator->eval_det_J( abscissa );
}
}

到目前为止,一切顺利。让我给你展示一下我的插值器类。

Interpolator.hpp

class Interpolator {
public:
/**
* Evaluate the determinant of the Jacobian (important for numerical integration).
*
* @note Common interface for all abscissa types.
*
* @param[in] abscissa Integration abscissa.
* @return Shape functions.
*/
virtual double
eval_det_J(
const Abscissa * abscissa ) const = 0;
};

从插值器中,我派生出所有几何形状的类。你可能注意到我使用了一个Interpolator_Template类作为基类。现在先忽略它,我马上会解释细节。该类包含所有四边形共有的函数。

Interpolator_Quad.hpp

class Interpolator_Quad : public Interpolator_Template< Abscissa_Quad > {
public:
// ... More functions common to all quadrilaterals.
};

这个派生类对应于这个问题开头所画的四边形。它被导出的原因是可能存在有更多插值节点的四边形。这个类实现了一个QUAD_04元素(一个有4个插值节点的四边形),但在有限元中,我们也有QUAD_08, QUAD_09等。

Interpolator_Quad_04.hpp

class Interpolator_Quad_04 final : public Interpolator_Quad {
public:
double
eval_det_J(
const Abscissa_Quad * abscissa ) const;
};

Interpolator_Quad_04.cpp

double
Interpolator_Quad_04::eval_det_J(
const Abscissa_Quad * abscissa ) const
{
// Important! Get coordinates from an Abscissa_Quad object.
const double xi = abscissa.xi;
const double eta = abscissa.eta;
double det_J = ...
// ... Perform some computations and return the determinant of the Jacobian.
return det_J;
}

让我回到我之前没有解释的Interpolator_Template类。在代码中的某个地方,我执行了从一个横坐标*到一个Abscissa_Quad *对象的向下转换。我是通过将模板类与非虚接口模式结合使用来实现这一点的。

Interpolator_Template.hpp

template< class Abscissa_Derived >
class Interpolator_Template : public Interpolator {
public:
/**
* Implements Interpolator::eval_det_J.
*/
double
eval_det_J(
const Abscissa * abscissa ) const;
protected:
/**
* Implemented by Interpolator_Quad_04 in this example.
*/
virtual double
eval_det_J(
const Abscissa_Derived * abscissa ) const = 0;
private:
Abscissa_Derived *
eval_abscissa(
const Abscissa * abscissa ) const;
};

Interpolator_Template.cpp

template< class Abscissa_Derived >
double
Interpolator_Template< Abscissa_Derived >::eval_det_J(
const Abscissa * abscissa ) const
{
Abscissa_Derived * abscissa_type = this->eval_abscissa( abscissa );
double det_J = this->eval_det_J( abscissa_type );
return det_J;
}
template< class Abscissa_Derived >
Abscissa_Derived *
Interpolator_Template< Abscissa_Derived >::eval_abscissa(
const Abscissa * abscissa ) const
{
// Dynamic cast occurs here.
// I will include some check later to check for nullptr.
return dynamic_cast< Abscissa_Derived * >( abscissa )
}

我确信这段代码包含错误,因为我必须复制和粘贴我认为是必要的,以表达我的观点,以及执行修改。然而,我希望我的想法能被正确地通过。

我知道向下转换通常是一种代码气味,所以在我开始在有限元中实现所有几何形状的积分器和插值器之前,我想听听你的意见。

我以前的尝试

这是我实现的最后一个设计模式。我将在下面解释我尝试过的其他设计;但是,您可以跳过阅读本节。

  • 一种双调度设计模式(特别是访问者模式),其中派生的是积分器而不是插补器。例如,我有一个Integrator_Quad_04而不是一个Interpolator_Quad_04。Integrator_Quad_04有一个Abscissa_Quad作为成员变量,因为abscissae不再被派生。

    class Integrator_Quad_04 final : public Integrator {
    private:
    std::vector< Abscissa_Quad * > _abscissae;
    public:
    double
    eval_det_J(
    const std::size_t &  index,
    const Interpolator * interpolator ) const
    {
    // The interpolator acts as the visitor.
    interpolator->eval_det_J( _abscissa[index] );
    }
    }
    /// Abscissa_Quad is no longer derived from Abscissa.
    class Abscissa_Quad {
    public:
    const double xi;
    const double eta;
    };
    
    然后,内插器成为积分器类的访问者,并访问其_abscissae成员变量。我决定不采用这种设计,因为那样的话,内插器将不得不根据操作来实现,而不是形状。
    class Interpolator {
    // ...
    };
    class Eval_Det_J : public Interpolator {
    double
    eval_det_J(
    const Abscissa_Quad * abscissa ) const;
    double
    eval_det_J(
    const Abscissa_Tria * abscissa ) const;
    };
    
  • 我试着做一些多调度的事情,但是所有形状所需的函数数量增长得非常快。

  • 双调度+模板的多种变体。

  • 我找到了我在这里使用的当前设计模式:

    面向对象设计问题,Liskov替代原理

  • 结论

正如您可能从代码中推断的那样,我正在使用c++ 11进行编译。

你可能想知道为什么我不简单地把积分器和插值器合并成一个类,答案是因为积分器可以在四边形的子域上操作。例如,我可以在四边形中引入一个虚构的三角形,并在三角形中放置积分点,但我仍然会使用四边形插值来近似三角形点内的解。当然,我需要在三角形和四边形坐标之间实现一些映射,但这是另一天的问题。

我想知道你是否认为向下投射不是解决这个问题的坏方法,或者我是否错过了什么。也许我没有解决这个问题的设计模式的知识,或者我的多态性体系结构是不正确的。

感谢任何反馈。谢谢!

我把这篇文章作为一个答案,因为评论太多了,也许它会有所帮助。

你可以让Integrator成为一个模板类:

template<class Shape>
class Integrator {
typedef typename Shape::AbscissaType AbscissaType;
public:
void integrate() const {
for (const AbscissaType& abscissa : _abscissae) {
_intepolator->eval_det_J(abscissa);
}
}
template<class OtherShape>
Integrator<OtherShape> convert(/* maybe pass range of abscissae indices */) const {
// Abscissae classes must have converting constructors like AbscissaTria(const AbscissaQuad&);            
std::vector<typename OtherShape::AbscissaType> newAbscissae(_abscissae.begin(), _abscissae.end());
// initialize resulting integrator with newAbscissae
}
Interpolator_Template<AbscissaType> _interpolator;
std::vector<AbscissaType> _abscissae;
};

如果仍然需要,则从适当的基模板继承特定的集成器:

class Integrator_Quad_04 : public Integrator<Quad> {
};

所以你不需要基本的Interpolator类和eval_det_J是一个非虚函数接受适当类型的横轴:

template<class AbscissaType>
class Interpolator_Template {
public:
double eval_det_J(const AbscissaType& abscissa) const;
}

我在这里添加了Shape,以强调横坐标类型取决于形状,您可能还有其他取决于形状的东西:

struct Tria {
typedef AbscissaTria AbscissaType;
static const size_t NUM_EDGES = 3; // just example
};
struct Quad {
typedef AbscissaQuad AbscissaType;
static const size_t NUM_EDGES = 4; // just example
};

我想你的代码中已经有这样的类了。

还需要注意的是,您也不再需要基础Abscissa,所有abscissae类都是独立的。

编辑:如果需要转换abscissae为不同的类型,可以在Integrator类中实现以下功能:
template<class OtherShape>
Integrator<OtherShape> convert(/* maybe pass range of abscissae indices */) const {
}

编辑2:将convert的示例实现添加到Integrator类中。