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 Circle
s, Ellipse
s and Triangle
s. All these objects have a common base 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 Circle
s and Ellipse
s. 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 Drawable
s 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?
radius
in your example, that could be aresize
method.