Object-oriented – Classic inheritance problem

inheritanceobject-orientedpolymorphism

I keep seeing the following pattern when people learn about OOP:

Problem: How do I put objects of different but related types into a container?

Solution: Inherit from a common base class.

New problem: I have a bunch of base class objects, how do I get the real type?

Solution: ???

The classic inheritance example is to make a graphics program that deals with geometric objects. We have Circles, Ellipses and Triangles. All these objects have a common base Drawable.

Drawable

In C++ this might look something like this:

struct Drawable{
    virtual ~Drawable(){}
    virtual void draw() const = 0;
};

struct Triangle : Drawable{
    Point p1, p2, p3;
    void draw() const override;
    void stretch(double xfactor, double yfactor);
    std::touple<double, double, double> getAngles() const;
    //...
};

struct Circle : Drawable{
    double radius;
    void draw() const override;
    //...
};

struct Ellipse : Drawable{
    double radius;
    void draw() const override;
    //...
};

std::vector<Drawable *> drawables = {new Circle, new Triangle, new Ellipse};
for (const auto &d : drawables)
    d->draw();

So far so good. Now I want to increase the radius of my Circles and Ellipses. As far as I know there are 4 ways to do that:

1: Put everything into the base class. The base class gets the virtual functions getAngles, getRadius, stretch and getEccentricity. There is a default implementation for each so that for example getRadius returns some dummy value when you are dealing with a Triangle. After this is done you can easily iterate over the Drawables and increase their radius. This makes the base class ridiculously huge and working on a vector<Drawable *> becomes inefficient because the functions do nothing for most elements.

2: Use dynamic_cast hacks.

for (const auto &d : drawables){
    if (dynamic_cast<Circle *>(d))
        ((Circle *)d)->radius++;
    else if (dynamic_cast<Ellipse *>(d))
        ((Ellipse *)d)->radius++;
}

Unfortunately you need to repeat this ugly syntax every time you need to do something like this. You cannot put it in a function or macro. Every time a new Drawable is added you have to go through the whole code base and add it where appropriate. Also the if/else-chain is fairly inefficient.

3: Add a Tag as the first member variable and cast accordingly.

for (const auto &d : drawables){
    switch(d->tag){
        case Tag::Circle:
            ((Circle *)d)->radius++;
            break;
        case Tag::Ellipse:
            ((Ellipse *)d)->radius++;
            break;
    }
}

Slightly more efficient due to a switch instead of an if/else-chain, but there is already a kind of tag that shows the dynamic type, so the tag is redundant. Also it has the same problem as 2: since you need to change the whole code base whenever a new Drawable comes along.

4: Just keep the objects in separate containers.

vector<Circle> circles;
vector<Triangle> triangles;
vector<Ellipse> ellipses;
for (auto &c : circles)
    c.radius++;
for (auto &e : ellipses)
    e.radius++;

This is very efficient memory layout wise and you only iterate over objects that matter. It is my favorite, but unfortunately it breaks the first solution in that you do not keep differently typed objects in a single container.

I tried to solve the problem by creating a variadic template that internally has a vector per type and forwards insertions to the right container. Ideally it would completely hide the fact that there are different containers under the hood. Usage would look something like this:

MultiVector<Triangle, Circle, Ellipse> mv;
mv.push_back(Triangle());
mv.push_back(Circle());
mv.push_back(Ellipse());
for (auto &d : mv.get<Circle, Ellipse>())
    d.radius++;

The solution is not perfect, for example different types do not keep their order and implementing the iteration is tricky.

I presented this problem and solution attempt to my (university) research group and got the reactions "If you design your classes right this is not a problem" and "The proposed solution is a very specific tool to solve a very specific problem and it is questionable at best to add even more complexity to c++ for such a minor detail". In my opinion this is a very basic problem that needs fixing and the usual solution of better class design only tries to find the least evil of the 4 workarounds in the specific case without actually solving the problem. The problem comes up almost every time inheritance is used.

So finally here is the question: Did you run into this problem too / is this a real problem? Do you know of another workaround/solution? Do other languages deal better with the problem? I want to simulate a poll using 2 comments below so feel free to upvote either "I never had this problem / The problem is easily solved." or "I also had this problem and never solved it to my satisfaction."

Edit: To prevent a close due to primarily opinion based / discussion: I want a clear answer that solves the problem or some resource that says that there is none.

Edit: Related: Do I really have a car in my garage?

Best Answer

This is a classic OOP issue, not that it signals that anything is wrong with the object oriented way of thinking, but it does require one to carefully consider the design.

When you wish to iterate over a container of objects, and alter some property that only a part of the objects have, and more importantly: only makes sense for a part of the objects, it is in my opinion the design that is the problem. The question here is, why do you want to alter some class specific property in a generic list? I am sure it make good sense in your situation, but think about it for a while.

You have a list of figures, this can be squares, rectangles, triangles, circles, polygons, etc. And now you wish to perform some action on them, which is fine, if they all support the action. But it does not make sense to alter a property on an object that would clearly not support it. It is counter-intuitive to iterate a list of figures, setting the radius, it is not however counter-intuitive to iterate a list of circles, setting the radius.

That does not mean, that some complicated, or let's just say smart design choice, can enable you to achieve this, but it goes against the concept of object orientation, by somehow trying to circumvent the encapsulation. I would argue, that this goes against the idea of polymorphism.

How to do it instead however is something different, which I am afraid I cannot produce an answer for at this moment. I do believe however, that you should do your utmost to try to avoid putting yourself in that situation.

One possible alternative you could look into is using a visitor pattern, where you have a visitor that accepts an object type that allows changing the radius. For instance:

class Visitor {
public:
  void Visit(Circle& circle) { /* Do something circle specific */ )
  void Visit(Square& square) { /* Do something square specific */ )
  // ...
};
Related Topic