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

601 views Asked by At

I am writing a scientific code which needs to create 3-dimensional cells, defined by a set of faces, which are defined by a set of vertices.

These 3 classes (Cell, Face, Vertex) are derived respectively from some generic geometry classes (Polyhedron, Polygon, Point) which implement some geometric routines like Polygon::CalculateArea().

The Face class adds to the Polygon class with additional data and functions required for the science, like Face::Interpolate(). I don't want to make these member functions virtual in the base class (Polygon).

Now, the problem. I initialize a Cell with a vector of pointers to Face, which is handled by the base class Polyhedron constructor, which upcasts the Face* to Polygon*:

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

Later, I want to access the Face* stored in a Cell so that I can call Face::Interpolate(), but it has been stored as a Polygon* and thus has no member function Polygon::Interpolate(). I can downcast it manually back to a Face* which works, but is not very clean. The user of the code has to do something like:

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

which is not obvious.

I want the interface to be transparent, so that this just works:

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

I can think of two or three ways to achieve this. I'm looking for a better solution or feedback of which of these is recommended:

  1. In Cell::GetFaces() which currently just inherits from Polyhedron::GetPolygons() I could create a wrapper that copies the std::vector<Polygon*> to a new vector std::vector<Face*>. This seems sloppy to me, not easy to maintain, inefficient and prone to errors.

  2. Instead of storing std::vector<Polygon*> I could store std::vector<std::shared_ptr<Polygon>>. From what I understand, these smart pointers retain type-awareness so that they can call the right destructor, but they might just store a reference to the destructor depending on implementation. I don't want to use shared_ptr for performance purposes -- I know they're good and friendly, but I'm creating millions of these Polygons and its easy to destroy them in the right place. I can't use unique_ptr easily because of the copy-constructor used in std::initializer_list constructors.

  3. Template the whole Polyhedron class, replacing every instance of Polygon* with F* and checking that F is a base of Polygon:

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

    and then inheriting from a parent with a given typename:

    class Cell : public Polyhedron<Face>
    

    This seems like the best method to me, since it has the least boilerplate and nothing exposed to the user; but it still feels messy, especially in the "real" case where there might be multiple types that would all have to be specified:

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

Is there a a better way? Perhaps a means of retaining type in the original vector (or some other container)?

If not, which of the above methods is the best practice and why?

Edit: Here's an abstracted view of the problem. The problem occurs when trying to run sumOfSomethingSpecific(). In my actual problem, that function is inside a derived class Derived_B, which is designed to work with Derived_A, but for the sake of the problem, it makes no difference.

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;
};
2

There are 2 answers

6
Raydel Miranda On

First most of issues you're facing here are not about programming, are about design.

... class with additional data and functions required for the science, like Face::Interpolate(). I don't want to make these member functions virtual in the base class (Polygon). ...

Well, don't do that, but then you have to realize that you're adding complexity to the code you need to implement such design desicion.

However, if every polygon can be "interpolated" then you should have a virtual function (or better yet a pure virtual function) in your Polygon class.

Said that, with the code as it is, in order to add transparency to the API you declare you get_* functions as:

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

that way is clear for the user that he/she has to provide a reference to a vector of faces to get the result. Lets see how this change your code:

// 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();

This way the down-cast is performed implicitly.

About your question: Is there a a better way? Yes, redesign your classes.

About your example:

I will ask you to think a moment about this:

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.
}

At some point you will have to tell wich type you want call the function on.

The level of abstraction you want, does not exists in C++. The compiler needs to know the type of an object in order to perform (compile) a call to one of its member functions.

Another approach you can try (I still recommend to redesign):

If you have the need of manipulating several distinct types in a uniform manner. Perhaps you want to take a look at Boot.Variant library.

3
Carlton On

I struggled with a similar problem in one of my projects. The solution I used was to give ownership of the actual objects to the most-derived class, give the base class a copy of the objects, and use a virtual function to keep the copy up-to-date as objects are added/removed:

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);
    }
};

This design has the benefits of clear ownership (most-derived class is owner), and that copying and upcasting the vector of object pointers is done only when needed. The downside is that you have two copies of essentially the same thing; a vector of pointers to objects. The design is very flexible too, since any class derived from Polyhedron only has to implement the RebuildPolygons() function, using a vector of any type derived from Polygon.