从Base*容器向下强制转换为Derived*容器,无需显式转换

Downcast from a container of Base* to Derived* without explicit conversion

本文关键字:容器 Derived 显式转换 转换 Base      更新时间:2023-10-16

我正在编写一个科学代码,需要创建三维细胞,由一组面定义,由一组顶点定义。

这3个类(Cell, Face, Vertex)分别是由一些一般的几何类(Polyhedron, Polygon, Point)推导而来,它们实现了一些类似Polygon::CalculateArea()的几何例程。

Face类在Polygon类的基础上增加了科学所需的额外数据和功能,如Face::Interpolate()。我不想让这些成员函数在基类(Polygon)中成为虚函数。

现在,问题是。我用指向Face的指针向量初始化Cell,这是由基类Polyhedron构造函数处理的,它将Face*向上转换为Polygon*:

Polyhedron::Polyhedron( std::initializer_list<Polygon*> polygons );

稍后,我想访问存储在Cell中的Face*,以便我可以调用Face::Interpolate(),但它已被存储为Polygon*,因此没有Polygon::Interpolate()成员函数。我可以手动将其下拉回Face*,但不是很干净。该代码的用户必须执行如下操作:

Face * temp_face = (Face*)cell->GetFaces()[0]; // Could use static_cast
temp_face->Interpolate();

这是不明显的

我希望界面是透明的,这样就可以工作了:

cell->GetFaces()[0]->Interpolate();

我可以想到两到三种方法来实现这一点。我正在寻找一个更好的解决方案或反馈建议:

  1. Cell::GetFaces(),目前只是从Polyhedron::GetPolygons()继承,我可以创建一个包装器,将std::vector<Polygon*>复制到一个新的矢量std::vector<Face*>。这对我来说似乎很草率,不容易维护,效率低下,容易出错。

  2. 我可以存储std::vector<std::shared_ptr<Polygon>>而不是存储std::vector<Polygon*>。据我所知,这些智能指针保留了类型意识,因此它们可以调用正确的析构函数,但它们可能只是存储对析构函数的引用,具体取决于实现。我不想为了性能目的而使用shared_ptr——我知道它们很好也很友好,但我创建了数百万个这样的多边形,很容易在正确的地方破坏它们。我不能轻易地使用unique_ptr,因为std::initializer_list构造函数中使用了复制构造函数。

  3. 模板整个多面体类,用F*替换Polygon*的每个实例,并检查FPolygon的基础:

    template<typename F = Polygon>
    typename std::enable_if<std::is_base_of<Polygon, F>::value, void>::type
    class Polyhedron
    

    ,然后从具有给定typename的父类继承:

    class Cell : public Polyhedron<Face>
    

    对我来说,这似乎是最好的方法,因为它具有最少的样板文件,并且没有向用户暴露任何内容;但是它仍然感觉很乱,特别是在可能有多个类型都必须指定的"实际"情况下:

    class Cell: public Polyhedron<Face,Vertex,type3,type4,type5,...>
    

有更好的方法吗?可能是在原始向量(或其他容器)中保留类型的一种方法?

如果不是,上面哪个方法是最佳实践,为什么?

编辑:

这是对这个问题的抽象看法。当尝试运行sumOfSomethingSpecific()时,会出现这个问题。在我的实际问题中,该函数位于派生类Derived_B中,该类设计用于与Derived_A一起工作,但为了解决问题,它没有区别。

class Base_A
{
public:
    Base_A();
    ~Base_A();
    // I don't want virtual doSomethingSpecific() here.
};
class Derived_A
{
public:
    using Base_A::Base_A;
    double doSomethingSpecific();
};
// I could template this whole class
// template <typename T>
// where T replaces Base_A
class B
{
public:
    // This can be initialized with:
    // std::vector<Derived_A*>
    // which is what I want to do, but we lose info about doSomethingSpecific()
    // even if I write a separate constructor its still stored as
    // std::vector<Base_A*>
    B(std::vector<Base_A*> v) : v(v) {};
    ~B();
    double sumOfSomethingSpecific()
    {
        double sum = 0;
        for(auto&& A : v) {
            // Can't do this, A is a pointer of type Base_A*, but this is the abstraction that I want to achieve
            sum += A->doSomethingSpecific();
            // Could do this, but its ugly and error-prone
            Derived_A* tempA = (Derived_A*)A;
            sum += tempA->doSomethingSpecific();
        }
        return sum;
    }
protected:
    std::vector<Base_A*> v;
};

首先,你在这里面临的大多数问题都不是关于编程,而是关于设计。

…类,提供科学所需的附加数据和功能,如Face::Interpolate()。我不想让这些成员函数在基类(Polygon)中成为虚函数. ...

好吧,不要这样做,但是你必须意识到你正在增加代码的复杂性,你需要实现这样的设计决策。

然而,如果每个多边形都可以"插值",那么你应该在你的Polygon类中有一个虚函数(或者更好的是一个纯虚函数)。

说,与代码一样,为了增加透明度的API,你声明你的get_*函数为:

void GetFaces(std::vector<Face *> &faces);

对于用户来说很清楚,他/她必须提供一个面向量的参考来获得结果。让我们看看这会如何改变你的代码:

// Face * temp_face = (Face*)cell->GetFaces()[0]; // Could use static_cast
std::vector<Face *> temp_faces;
cell->GetFaces(temp_faces);
//temp_face->Interpolate();
temp_faces[0]->Interpolate();

这种方式隐式地执行向下强制转换。

关于你的问题:有没有更好的方法?是的,重新设计你的类。

关于你的例子:

请大家思考一下:

struct Base {};
struct Derived_A: Base { double doSomethingSpecific(); };
struct Derived_B: Base { double doSomethingSpecific(); };
int main()
{
    std::vector<Base*> base_v = {/*suppose initialization here*/};
    base_v[0]->doSomethingSpecific();  // Which function must be called here? 
                                       // Derived_A::doSomethingSpecific or
                                       // Derived_B::doSomethingSpecific.
}

在某些时候,你必须说明你想在哪个类型上调用函数。

你想要的抽象层次在c++中并不存在。编译器需要知道对象的类型,以便执行(编译)对其成员函数的调用。

您可以尝试另一种方法(我仍然建议重新设计):

如果需要以统一的方式操作几个不同的类型。也许你该去看看Boot。变种库。

我在我的一个项目中遇到了类似的问题。我使用的解决方案是将实际对象的所有权赋予最派生的类,为基类提供对象的副本,并使用虚函数在添加/删除对象时保持副本的最新状态:

class Polyhedron {
protected:
    bool _polygons_valid = false;
    std::vector<Polygon*> _polygons;
    virtual void RebuildPolygons() = 0;
public:
    std::vector<Polygon*>& GetPolygons()
    {
        if (!_polygons_valid) {
            RebuildPolygons();
            _polygons_valid = true;
        }
        return _polygons;
    }
    /*Call 'GetPolygons()' whenever you need access to the list of polygons in base class*/
};
class Cell: public Polyhedron {
private:
    std::vector<Face*> _faces;  //Remember to set _polygons_valid = false when modifying the _faces vector.
public:
    Cell(std::initializer_list<Face*> faces):
        _faces(faces) {}
    //Reimplement RebuildPolygons()
    void RebuildPolygons() override
    {
        _polygons.clear();
        for (Face* face : _faces)
            _polygons.push_back(face);
    }
};

这种设计的好处是所有权清晰(大多数派生类都是所有者),并且只有在需要时才复制和上转换对象指针的向量。缺点是你有两个本质上相同的东西的副本;指向对象的指针向量。设计也非常灵活,因为任何从Polyhedron派生的类只需要实现RebuildPolygons()函数,使用从Polygon派生的任何类型的向量。