Upcoming Events
Southwest Gaming Expo
11/20 - 11/22 @ Dallas, TX

Workshop on Network and Systems Support for Games (NetGames 2009)
11/23 - 11/25 @ Paris, France

ICIDS 2009 Interactive Storytelling
12/9 - 12/11 @ Guimarães, Portugal

Global Game Jam
1/29 - 1/31  

More events...


Quick Stats
6740 people currently visiting GDNet.
2341 articles in the reference section.

Help us fight cancer!
Join SETI Team GDNet!



Link to us

Link to us

  Intel sponsors gamedev.net search:   

We Can Rebuild It; We Have The Technology

Now that we have this all set up, let's rip it apart! Let's tear it down and rebuild it into a class structure which invites compile-time polymorphism.

How shall we do this? First, let's remove the virtual keyword from the declarations of DrawOutline() and DrawFill(). As we touched on earlier, virtual functions add runtime overhead which is precisely what we are trying to avoid. For that matter, let's go one step further and remove the declarations of those functions from the base class altogether, as they do us no good anyway. Let's leave them in as comments, though, so that it remains clear that they were omitted on purpose.

Now, what have we broken? Not much, actually. If we have a Rectangle, we can get and set its height and width and colors and location, and we can draw it. Life is good. However, one thing that we have broken is the DrawFilled() function, which calls the now nonexistent base class functions DrawOutline() and DrawFill(). Base classes can only call functions of derived classes if those functions are declared as virtual in the base class--which is precisely what we do not want.

In order to fix the broken DrawFilled() function, we will use templates in a very strange and interesting way. Here's a bit of code to broadly illustrate the insanity that is to come.

template <typename ShapeType>
class Shape
{

...
protected:
  Shape( ... )
  {
  }

};

class Rectangle : public Shape<Rectangle>
{

public:
  Rectangle( ... ) :
    Shape<Rectangle>( ... )
  {
  }
...

};

Whaaa? That's right: Rectangle no longer inherits from Shape; now it inherits from a special kind of Shape. Rectangle creates its own special Shape class, Shape<Rectangle>, to inherit from. In fact, Rectangle is the only class that inherits from this specially crafted Shape<Rectangle>. To enforce this, we declare the constructor of the templated Shape class protected so that an object of this type can not be instanced directly. Instead, this special kind of Shape must be inherited from and instanced within the public constructor of the derived class.

Yes, it's legal. Yes, it's strange. Yes, it's necessary. It's called the "Curiously Recurring Template Pattern" (or Idiom, depending on who you ask).

But why? What could this strange code possibly gain us??

What we gain is the template parameter. The base class Shape now knows that it really is the Shape part of a Rectangle because we have told it so through the template parameter, and because we have taken a solemn oath that the only class that ever inherits Shape<Rectangle> is Rectangle. If Shape<ShapeType> ever wonders what subclass it's a part of, it can just check its ShapeType template parameter.

What this knowledge gains us, in turn, is the ability to downcast. Downcasting is taking an object of a base class and casting it as an object of a derived class. It's what dynamic_cast does for virtual classes, and it's what virtual function calls do. It's also what we tried to do way back near the beginning of this article, when we tried to use reinterpret_cast to convince our compiler that myShape was a Rectangle. Now that the functions aren't virtual anymore, however, this will work much better (in other words, it'll work). Let's use it to rewrite DrawFilled().

template <typename ShapeType>
class Shape
{
  void DrawFilled()
  {
    reinterpret_cast<const ShapeType *>(this)->DrawOutline();
    reinterpret_cast<const ShapeType *>(this)->DrawFill();
  }
};

Take a moment to cogitate on this code. It's possibly the most crucial part of this entire article. When DrawFilled() is called on a Rectangle, even though it is a method defined in Shape and thus called with a this pointer of type Shape, it knows that it can safely treat itself as a Rectangle. This lets Shape reinterpret_cast itself down to a Rectangle and from there call DrawOutline() on the resultant Rectangle. Ditto with DrawFill().

Putting It Together

So let's put it all together.

template<typename ShapeType>
class Shape
{
public:
  ~Shape()
  {
  }

/* Omitted from the base class and
   declared instead in subclasses */
/* void DrawOutline() const = 0; */
/* void DrawFill() const = 0; */

  void SetOutlineColor(const std::string &newOutlineColor)
  {
    outlineColor = newOutlineColor;
  }

  void SetFillColor(const std::string &newFillColor)
  {
    fillColor = newFillColor;
  }

  void SetLocation(const Point &newLocation)
  {
    location = newLocation;
  }

  const std::string &GetOutlineColor() const
  {
    return outlineColor;
  }

  const std::string &GetFillColor() const
  {
    return fillColor;
  }

  const Point &GetLocation() const
  {
    return location;
  }

  void DrawFilled() const
  {
    reinterpret_cast<const ShapeType *>(this)->DrawOutline();
    reinterpret_cast<const ShapeType *>(this)->DrawFill();
  }

protected:
  Shape(const Point &initialLocation,
     const std::string &initialOutlineColor,
     const std::string &initialFillColor) :
    location(initialLocation),
    outlineColor(initialOutlineColor),
    fillColor(initialFillColor)
  {
  }

private:
  std::string outlineColor;

  std::string fillColor;

  Point location;
};

class Rectangle : public Shape<Rectangle>
{
public:
  Rectangle(const Point &initialLocation,
        const std::string &initialOutlineColor,
        const std::string &initialFillColor,
        double initialHeight,
        double initialWidth) :
    Shape<Rectangle>(initialLocation, initialOutlineColor,
                     initialFillColor),
    height(initialHeight),
    width(initialWidth)
  {
  }

  ~Rectangle()
  {
  }

  void DrawOutline() const
  {
    Graphics::SetColor(GetOutlineColor());
    Graphics::GoToPoint(GetLocation());
    Graphics::DrawRectangleLines(height, width);
  }

  void DrawFill() const
  {
    Graphics::SetColor(GetOutlineColor());
    Graphics::GoToPoint(GetLocation());
    Graphics::DrawRectangleFill(height, width);
  }

  void SetHeight(double newHeight)
  {
    height = newHeight;
  }

  void SetWidth(double newWidth)
  {
    width = newWidth;
  }

  double GetHeight() const
  {
    return height;
  }

  double GetWidth() const
  {
    return width;
  }

private:
  double height;
  double width;
};

class Circle : public Shape<Circle>
{
public:
  Circle(const Point &initialLocation,
      const std::string &initialOutlineColor,
      const std::string &initialFillColor,
      double initialRadius) :
    Shape<Circle>(initialLocation, initialOutlineColor,
                  initialFillColor),
    radius(initialRadius)
  {
  }

  ~Circle()
  {
  }

  void DrawOutline() const
  {
    Graphics::SetColor(GetOutlineColor());
    Graphics::GoToPoint(GetLocation());
    Graphics::DrawCircularLine(radius);
  }

  void DrawFill() const
  {
    Graphics::SetColor(GetOutlineColor());
    Graphics::GoToPoint(GetLocation());
    Graphics::DrawCircularFill(radius);
  }

  void SetRadius(double newRadius)
  {
    radius = newRadius;
  }

  double GetRadius() const
  {
    return radius;
  }
private:
  double radius;
};

This is just what we need! Base class functions can defer certain functionality to derived classes and derived classes can decide which base class functions to override. If we had declared a non-virtual DrawOutline() function in Shape (rather than leaving it in only as a comment), it would be optional for Circle and Rectangle to override it. This approach allows programmers using a class to not concern themselves with whether a function is in the derived class or inherited from the base class. It's the functionality that we had in the last section, but without the added overhead of run-time polymorphism.

While we're at it, let's rewrite DrawAShapeOverAndOver().

template<typename ShapeType>
void DrawAShapeOverAndOver(ShapeType* myShape)
{
  for(int i=0; i<10000; i++)
  {
    myShape->DrawOutline();
    // OR
    myShape->DrawFilled();
  }
}

Rectangle *rectangle = new Rectangle;
DrawAShapeOverAndOver(rectangle);
delete rectangle;

Notice that we can call member functions declared either in the derived class or the base class. Of course, if the templated function uses member functions defined in only a particular derived class (such as GetRadius()), the templated function will not compile if used with a class that does not have those member functions. For example, calling GetRadius() on a Rectangle will not compile.

Limitations

The biggest limitation of compile-time polymorphism is that it's compile-time. In other words, if we want to call a function on a Rectangle, we can't do it through a pointer to a Shape. In fact, there is no such thing as a pointer to a Shape, since there is no Shape class without a template argument.

This is less of a limitation than you might think. Take another look at our rewritten DrawAShapeOverAndOver():

template<typename ShapeType>
void DrawAShapeOverAndOver(ShapeType* myShape)
{
  for(int i=0; i<10000; i++)
  {
    myShape->DrawOutline();
  }
}

Essentially, wherever you once had functions that took in base class pointers, you now have templated functions that take in derived class pointers (or derived classes). The responsibility for calling the correct member function is delegated to the outer templated function, not to the object.

Templates have drawbacks. Although the best way to get a feel for these drawbacks is to experience them yourself, it's also a good idea for a programmer to have an idea of what to expect. First and foremost is that most compilers require templates to be declared inline. This means that all your templated functions will have to go in the header, which can make your code less tidy. (If you're using the Comeau compiler, this doesn't apply to you. Congratulations.)

Secondly, templates can lead to code bloat, since different versions of the functions must be compiled for each datatype they are used with. How much code bloat is caused is very specific to the project; switching all of my content loading functions to use this model increased my stripped executable size by about 50k. As always, the best source of wisdom is your own tests.

Summary

Using templates for compile-time polymorphism can increase performance when they are used to avoid needless virtual function binding. With careful design, templates can be used to give non-virtual classes all the capabilities that virtual classes have, except for runtime binding. Although such compile-time polymorphism is not appropriate for every situation, a careful decision by the programmer as to where virtual functions are actually needed can dramatically improve code performance, without incurring a loss of flexibility or readability.




Contents
  Introduction
  Thanks But No Thanks, C++
  We Can Rebuild It; We Have The Technology

  Printable version
  Discuss this article