C++ Common Knowledge: Capability Queries
Most times when an object shows up for work, it's capable of performing as required, because its capabilities are advertised explicitly in its interface. In these cases, we don't ask the object if it can do the job; we just tell it to get to work:
class Shape { public: virtual ~Shape(); virtual void draw() const = 0; //... }; //... Shape *s = getSomeShape(); // get a shape, and tell it to... s->draw(); // ...get to work!
Even though we don't know precisely what type of shape we're dealing with, we know that it is-a Shape and, therefore, can draw itself. This is a simple and efficient—and therefore desirable—state of affairs.
However, life is not always that straightforward. Sometimes an object shows up for work whose capabilities are not obvious. For example, we may have a need for a shape that can be rolled:
class Rollable { public: virtual ~Rollable(); virtual void roll() = 0; };
A class like Rollable is often called an "interface class" because it specifies an interface only, similar to a Java interface. Typically, such a class has no non-static data members, no declared constructor, a virtual destructor, and a set of pure virtual functions that specify what a Rollable object is capable of doing. In this case, we're saying that anything that is-a Rollable can roll. Some shapes can roll; others can't:
class Circle : public Shape, public Rollable { // circles roll //... void draw() const; void roll(); //... }; class Square : public Shape { // squares don't //... void draw() const; //... };
Of course, other types of objects in addition to shapes may be rollable:
class Wheel : public Rollable { ... };
Ideally, our code should be partitioned in such a way that we always know whether we are dealing with objects that are Rollable before we attempt to roll them, just as we earlier knew we were dealing with Shapes before we attempted to draw them.
vector<Rollable *> rollingStock; //... for( vector<Rollable *>::iterator i( rollingstock.begin() ); i != rollingStock.end(); ++i ) (*i)->roll();
Unfortunately, we occasionally run up against situations where we simply do not know if an object has a required capability. In such cases, we are forced to perform a capability query. In C++, a capability query is typically expressed as a dynamic_cast between unrelated types (see New Cast Operators [9, 29]).
Shape *s = getSomeShape(); Rollable *roller = dynamic_cast<Rollable *>(s);
This use of dynamic_cast is often called a "cross-cast," because it attempts a conversion across a hierarchy, rather than up or down a hierarchy, as shown in Figure 6.
Figure 6 A capability query: "May I tell this shape to roll?"
If s refers to a Square, the dynamic_cast will fail (result in a null pointer), letting us know that the Shape to which s refers is not also Rollable. If s refers to a Circle or to some other type of Shape that is also derived from Rollable, then the cast will succeed, and we'll know that we can roll the shape.
Shape *s = getSomeShape(); if( Rollable *roller = dynamic_cast<Rollable *>(s) ) roller->roll();
Capability queries are occasionally required, but they tend to be overused. They are often an indicator of bad design, and it's best to avoid making runtime queries about an object's capabilities unless no other reasonable approach is available.