C++ A problem with polymorphism

Started by
12 comments, last by Glak 18 years, 5 months ago
The title is a bit vague because I don't actually know what this problem is called. I will give an example of the problem. Below is a simple shape class heirarchy. I have left a lot necessary things out I know. It is just an example:


class CShape
{
  virtual void dummy()=0;
};

class CSquare : public CShape
{
  void setRadius();
}

class CCircle : public CShape
{
  ..
}





Okay now say that I have a container of shape pointers and I fill it with various shapes:

  vector<CShape*> shapes;
  
  shapes.push_back(new CSquare());
  shapes.push_back(new CCircle());





Now here is the problem: What if I want to iterate through the shapes in that container and call the setRadius method on each circle? Ie:

for (int i=0;i<shapes.size();i++)
{
  shapes->setRadius();
}





The problem is that not all the shapes have to be circles. Some might be squares. At run time I do not know. If one of the shapes is a square, then the call to setRadius() could fail. There are several options I can think of getting round this problem, and I was just wondering if anyone had any ideas. 1st solution Abstract class CShape defines a virtual member function called ID. Derived classes must implement this function to return a unique ID. Therefore I could do this:

for (int i=0;i<shapes.size();i++)
{
  if (shapes.ID() == CIRCLE)
    shapes->setRadius();
}





2nd solution Use RTTI (which I only just found out existed) to determine the type of shape:

for (int i=0;i<shapes.size();i++)
{
  if (CCircle*cptr = dynamic_cast <CCircle*>(shapes))
    shapes->setRadius();
}





3rd solution Implement an empty setRadius function in the base CShape class, it is then optional whether derived classes override it, but at least it can be called by all shapes without failing.

class CShape
{
  virtual void dummy()=0;
  void setRadius(); //this class will be implemented as empty
};

for (int i=0;i<shapes.size();i++)
{
  shapes->setRadius();
}



This is my least favorite method as it just seems wrong to me. Not all shapes have a radius property, so in my opinion it shouldn't be part of a base shape class. Perhaps someone has a better method of doing this? (this is what I hope). Just to note that the generic shape container is required, it is not an option to have seperate containers for each type of shape. Wouldn't it be nice if when calling setRadius() on an object, if that method didn't exist on that class then it was just not called? Thanks for reading
Advertisement
You shouldn't want to do that in the first place. The basic idea behind storing a list of Shapes [rather than a list of Circles or Squares] is that you are stating that you only care about the common interface. If you care about the radius of your shapes, then you should maintain a list of Circles instead.

CM
The visitor pattern is what you are looking for.

Or a language with multi dispatch ;)
Depending on what your trying to do, you could also pass the radius to the circle constructors (you'd need to write the needed constructor of course).
You seem to have a failure of abstraction in your program. The question becomes why are you calling setRadius()? Chances are when you perform the setRadius() call you're actually doing an more abstract operation over all the elements in the container. If that's the case then that abstract operation should be made a virtual member function of the CShape class and the setRadius() call be made part of the implementation of the CCircle class' version.
Okay it seems my mistake is a matter of design rather than implementation, and lots of you are suggesting changes in implementation, and they do seem like valid changes in the case of the shapes example, but I am not actually working on a project to do with shapes - that was just an example for my question of which implementation was better.

What I am working on is a 3D Tile engine, and the problem I have is to do with tiles. So perhaps if I explain that instead it will be better.

The terrain map contains a 2D array of tiles. Each tile holds pointers to it's neighbouring tiles. A consequence of the model I am using is that edge tiles are not active parts of the map. This means they will not be drawn, and objects cannot move into them. They are only there as placemarkers for the edge of the map. So there are two types of tile: EdgeTiles, and TrueTiles. Truetiles must have a number of methods which EdgeTiles will not (such as draw()).

I should be able to do:

Tile* mytile = maptile[40][30];
Tile* myneighbour = mytile->neighbour[NORTH];
myneighbour->Draw();

But if myneighbour is an EdgeTile this will crash. I cannot know this in advance. I do not want to explicitly test the X and Y coordinates of the tiles to determine if they are edges.

So what would be the best class heirarchy for this? Perhaps EdgeTile should be a base class, and TrueTile should extend that base class. But I still have this problem with the heterogeneous container.
how about having only one tile class, and having the edge tiles have NULL pointers to their non-existant neighbours.

your code would be like this

Tile* mytile = maptile[40][30];Tile* myneighbour = mytile->neighbour[NORTH];if( myneighbour != NULL )   myneighbour->Draw();
That code should not even compile, unless you are doing some casting that you didn't show us. You cannot call the draw method on the Tile class if it does not define it.
I'd look into making neighbor not return a valid tile if there is no neighbor in that direction. Either a null pointer that you can check for, or a smart pointer containing an edge tile that dies when you stop using it. Then make Draw virtual to be a no-op for edge tiles or if(!this)

But as others say, base classes for heterogeneous containers are often best being simply interfaces.
Why are edge tiles a distinct type? It seems as though the easiest solution is to just not assign those pointers at all, and drop the entire concept of an edge tile.
Tile* myneighbour = mytile->neighbour[NORTH];if(myNeighbor != 0)   myneighbour->Draw();

CM

This topic is closed to new replies.

Advertisement