The guideline
Polymorphism should generally be preferred over switch statements which check for a type, because the resulting code is much more type-safe and you get compile-time errors instead of runtime-errors which is generally a good thing. You have the interesting case of a function which takes two objects and which should be dispatched depending dynamically on both argument types.
This is how it's done
Here's one way how to do that: First you need to forward declare all derived classes and write the base class in the following way:
class Box;
class Sphere;
class Cone;
// ...
class GeometryBase
{
public:
virtual bool collidesWith( const GeometryBase & other ) const = 0;
protected:
virtual bool dispatchCollidesWith( const Box & other ) const = 0;
virtual bool dispatchCollidesWith( const Sphere & other ) const = 0;
virtual bool dispatchCollidesWith( const Cone & other ) const = 0;
// ...
};
The function collidesWith() must be implemented to call the dispatchCollidesWith() on other with *this as the argument. Note that *this has different types in the derived classes and hence the different overloads are called. In order to omit too much typing we use a template which does the implementation for us:
template <typename T>
class GeometryImpl : public GeometryBase
{
public:
virtual bool collidesWith( const GeometryBase & other ) const
{
assert( typeid(*this) == typeid(T) );
return other.dispatchCollidesWith( static_cast<const T&>(*this) );
}
};
Now the derived classes can be implemented in the following way:
class Box : public GeometryImpl<Box>
{
protected:
virtual bool dispatchCollidesWith( const Box & other ) const { /* do the math */ }
virtual bool dispatchCollidesWith( const Sphere & other ) const { /* do the math */ }
virtual bool dispatchCollidesWith( const Cone & other ) const { /* do the math */ }
// ...
private:
// data ...
};
Given two geometries geom1 and geom2 you can now test for collision with
geom1.collidesWith( geom2 );
And everything is perfectly type-safe.
A more extensible pattern
There's a backside to this approach: You have to add tons of functions to your base class and it will get crowded easily. Here's how you can make your base class extensible for virtual dispatch, so you don't need to add a virtual function to it everytime you need new functionality:
class GeometryDispatcher;
class GeometryBase
{
public:
void run( GeometryDispatcher & dispatcher ) const = 0;
};
class GeometryDispatcher
{
public:
virtual void dispatch( const Box & shape ) = 0;
virtual void dispatch( const Sphere & shape ) = 0;
virtual void dispatch( const Cone & shape ) = 0;
};
By deriving from GeometryDispatcher you can add a new functionality to your class hierarchy. The run() function must be implemented by the derived classes of GeometryBase in the following way:
void Box::run( GeometryDispatcher & dispatcher ) const
{
dispatcher.dispatch( *this );
}
(This is also known as the first half of the visitor pattern. Read about it, so you can understand my code!) Now you could add a function collidesWithBox() in the following way
class CollidesWithBoxDispatcher : public GeometryDispatcher
{
public:
CollidesWithBoxDispatcher( const Box & box ) : box(box) {}
bool getResult() const { return result; }
virtual void dispatch( const Box & shape ) { ... }
virtual void dispatch( const Sphere & shape ) { ... }
virtual void dispatch( const Cone & shape ) { ... }
private:
bool result;
const Box & box;
};
bool collidesWithBox( const GeometryBase & shape, const Box & box )
{
CollidesWithBoxDispatcher d( box );
shape.run( d );
return d.result;
}
You can implement the functions collidesWithSphere() and collidesWithCone() analogously. Finally, you can implement the function collidesWith() this way:
class CollidesWithDispatcher : public GeometryDispatcher
{
public:
CollidesWithDispatcher( const GeometryBase & shape ) : shape(shape) {}
bool getResult() const { return result; }
virtual void dispatch( const Box & box )
{
result = collidesWithBox( shape, box );
}
virtual void dispatch( const Sphere & sphere ) { ... }
virtual void dispatch( const Cone & cone ) { ... }
private:
bool result;
const GeometryBase & shape;
};
bool collidesWith( const GeometryBase & shape1, const GeometryBase shape2 )
{
CollidesWithDispatcher d( shape2 );
shape1.run( d );
return d.result;
}
A lot of code to write but you get a double dispatch this way by facilitating the visitor pattern. Happy end. :)