2

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?

4

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 */ )
  // ...
};
  • ...but it goes against the concept of object orientation, by somehow trying to circumvent the encapsulation...One possible alternative you could look into is using a visitor pattern... If the idea of taking a decision based on the object's type bothers you, why recommend a really complicated way of doing it? – Doval Jul 12 '14 at 2:47
  • 1
    @Doval: Because it's a "design pattern". It must be right if it's blessed by GoF, right? – cHao Jul 12 '14 at 2:53
  • @Doval I honestly do not think that a visitor pattern is a complicated way of solving problem like this. – Tommy Andersen Jul 12 '14 at 14:12
  • 1
    @Doval Actually I find that a visitor pattern will give you far less coupling, than say an enum-like tag, which either require a cast, or union as in your case. – Tommy Andersen Jul 12 '14 at 14:15
2
Problem: How do I put objects of different but related types into a container?

Solution: Inherit from a common base class.

This is the problem. If your design called for separate types, you shouldn't have to merge them for the sake of putting them into a container. You chose an example where no one will look at you funny if you merge them all under a common base class, but this solution clearly doesn't work when the types have less in common, or are primitive/scalar types, or are third party types you don't control.

What you want is a tagged union. C/C++ doesn't have these, but it does have untagged unions which you can tag yourself:

enum ShapeType { TRIANGLE, CIRCLE, ELLIPSE };

struct ShapeUnion {
    ShapeType type;
    union {
       Triangle triangle;
       Circle circle;
       Ellipse ellipse;
    } data;
};

By checking the enum you can determine which member of the union to access:

std::vector<ShapeUnion> shapes = getShapes();
for (auto& shape: shapes) {
    switch (shape.type) {
        case TRIANGLE: foo(shape.data.triangle); break;
        case CIRCLE: bar(shape.data.circle); break;
        case ELLIPSE: baz(shape.data.ellipse); break;
        default: // raise some sort of error
    }
}

I'm sure you can find templates for this if you google a bit.

Disclaimer: I don't write much C/C++, the above code may contain syntax errors.

  • 1
    That tagged union looks quite a bit like solution #3 if you squint just right... – J Trana Jul 12 '14 at 2:45
  • @JTrana It's similar, except I'm not using inheritance nor modifying the original data types. The union could contain, say, a Triangle or an int if you wanted to. – Doval Jul 12 '14 at 2:54
  • 1
    @Doval So whenever a new type is created, you would need to ad it to the enum, the union and the switch cases. Isn't this a problem that can be solved using inheritance, in a way that requires less coupling? – Tommy Andersen Jul 12 '14 at 14:18
  • @TommyA Less coupling? Inheritance is one of the tightest forms of coupling there is. The question is about retrieving the type of a union (which an inheritance tree is) and presumably acting on it; you won't find a solution that'll allow you to add new types to the union and not have to fix the rest of the code, just like you won't find a way to add new methods to an interface without having to fix the classes that implement it. – Doval Jul 12 '14 at 15:58
1

So here's the issue: what's classical inheritance really all about? Taxonomy! Strict hierarchies and the like. Hierarchies work based on commonality of the types. So now you're trying to say, "I want to group unlike things together". Ok, you can do that. But since they're not the same type, a problem solving pattern based on similarity isn't likely to work well for acting on them as a group ... unless you find the way in which they are similar. If they really are similar, you can act on them as a group in some abstract way by making a base class and you shouldn't need to switch on type. If they're not similar at all, you can't act on them without switching on the type. And really, let's face it: polymorphism really is doing this by switching which implementation is called under the hood, so in some odd way it too is like a switch statement - just faster and more elegant.

So when you think about it this way, you've got to put the switch either into the class itself (classical inheritance, probably with some strange "DoStuff()" or "Process()" method) or into the the thing acting on the class (visitor pattern or switch on types) or segregate the lists (solution #4). The advantage of the visitor/switch on type is that it doesn't muddy up your base class and is therefore nicer for modular code.

Now OOP isn't the only paradigm in town. Game designers often have to think about objects that don't form a strict hierarchy, but rather have certain "attributes". You might find component design or related variants interesting if you run into this type of problem often.

1

I'm personally perfectly fine using downcasting. Simply because it is same as language-aided visitor pattern. It becomes even better if you create HasRadius interface/abstract class, that allows you to specify which classes do allow this change. This then allows you to add it to potentially new Drawable and eliminates the if chain. And some of the complains about this solution can be solved by implementing actual visitor, because changes to types will be ensured to be correct by the type system and compiler.

And one thing to note: you talk a lot about efficiency. Programming is about tradeofs and abstractions (especially those that add Open/Closed properties to design) always go directly against efficiency. I'm not saying efficiency is not necessary. Just that when you start using OOP, you have to be able to accept some performance hit. And lets not forget : You should leave performance optimizations after you profile your code. Thinking about performance in such example is going to skewer the results extremely.

0

You asked:

Did you run into this problem too / is this a real problem? Do you know of another workaround/solution?

My answer to that is a big YES. I deal with this a lot at my work.

You said,

Now I want to increase the radius of my Circles and Ellipses.

The key to your solution lines in delving into the details of that requirement. There is no "I" in code. You have to identify which parts of your code needs to perform that operation. That section of your code must know about Circles and Ellipses. If that section of the code knows about Circles and Ellipses, they can take a base class pointer or reference and perform a dynamic_cast. If the dynamic_cast succeeds, they can call the member functions directly. If the dynamic_cast does not succeed, they can ignore that object, throw an exception, or deal with it some other way.

A generic approach

We have dealt with generic properties using string based keys and generic values. For example, a generic property handler associated with Circles and Ellipses would register itself indicating that it knows how to deal with the "radius" properties of objects. A generic dispatch mechanism would call that property handler when it needs to set the "radius" of a Drawable.

In pseudo code, you would have:

void setProperty(Drawable& dr, std::string const& key, std::string const& value)
{
    PropertyHandler* handler = getPropertyHandler(key);
    if ( handler != NULL )
    {
       handler.setProperty(dr, key, value);
    }
}

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.