MPL Madness

August 31, 2009

The time has finally come. For years now I’ve known that of all my C++ shortcomings, the most obvious is my irrational fear of Template Metaprogramming. I’ve been telling myself that when the C++0x standard comes out, Concepts will change the face of metaprogramming, so I should just keep avoiding it until I can learn the new system. Well as Sutter reports, Concepts is out of the (newly renamed) C++1x Standard, and might not make it into the language for another few years. So it’s time for me to bite the bullet and learn something new.

With that in mind I’ve bought the big scary book, C++ Template Metaprogramming and am trying to read it. In the first weekend I managed to get through chapter 1 (mostly background) and struggle through chapter 2. Then I attempted the exercises in chapter 2 and almost gave myself a stroke. I got through half of them before moving on to chapter 3. A few pages into chapter 3 I was bleeding from the ears, so I backtracked and re-read chapter 2. Long story short, it’s gonna take me a lifetime to get through this book.

However I have managed to come up with an example where MAYBE I have put it to good (or at least interesting) use. I’ve been toying with the idea of re-writing the game engine on which Rift is built. So today I thought I’d tinker around with the math a bit, see if anything is interesting. One of the things I’m trying to do is create specific classes for specific purposes (I’m tired of losing track of whether or not my rotation vector is relative to the global coordinate space or the object’s). So I’ve been trying to come up with ways to create 1 template that can then be tricked into being different types for different purposes. So with that in mind, let’s take a look at the humble Vector.

For purposes of this discussion there are 2 kinds of Vectors, regular ones and normalized ones. Obviously a normalized one has a length that’s always 1. So the trick was to write a vector class that can be 2 types, one for regular vectors and one for normal vectors. Before templates I would just include a run time flag, m_IsNormal, and check it to see if I should normalize. Not a bad solution, but a bit prone to error and chews up runtime cycles (okay not a lot, but bear with me). So how do I create a vector that picks the correct type at compile time and normalizes accordingly? Here’s how I started:

template class Vector
{
public:
    Vector( float X, float Y, float Z )
      : m_X( X )
      , m_Y( Y )
      , m_Z( Z )
    {
    }
    void Normalize();
private:
    float m_X, m_Y, m_Z;
};

This works pretty well as a regular old Vector. But as you can see, it doesn’t normalize unless you specifically tell it to. So it fails as a Normalized Vector. I could call Normalize in the constructor, but of course then it would always normalize, which would make it fail as a plain vector. So I decided that I needed was a static helper class:

template struct Normalizer;

And of course, what good is a static helper class if it doesn’t perform an action? And not only that, but we have to be able to select that action at compile time. Thankfully this is all what I learned in chapter 2 of the book. You can select different actions by creating different specializations of the static helper class:

template
struct Normalizer
{
    static void Perform( Vector& V )
    {
        V.Normalize();
    }
};

template
struct Normalizer
{
    static void Perform( Vector& V )
    {    }
};

How’s that look? Now if you compile a Normalizer with true then it will call Normalize, and if you compile it with false, it won’t. Just what we need. How do we use it? I’m going to change the constructor a bit:

    Vector( float X, float Y, float Z )
      : m_X( X )
      , m_Y( Y )
      , m_Z( Z )
    {
        Normalizer::Perform( *this );
    }

Do you see the magic? When you set ShouldNormalize to true at compile time it will compile the version of Normalizer that calls Normalize(). If you set ShouldNormalize to false, it will do nothing. My hope is that since it’s a static function the optimizer will get rid of the function call and efficiency will be obtained.

But WAIT! There’s a problem. Now we have 2 vectors that are completely different types. Logically they’re both vectors, one’s just stored normalized, but to the compiler they’re completely different things. So the following reasonable code will fail:

typedef Vector PlainVector;
typedef Vector NormalizedVector;

PlainVector Plain( 10, 20, 10 );
NormalVector Normalized = Plain;

Damn! So close. So what do we do now? Well this is the point where I start to wonder if my solution is clever, or horrible. I hope at the very least it’s interesting. I figured what I needed was a copy constructor to convert between the two types. The clever part is that I only have to write one copy constructor to achieve this goal:

    Vector( const Vector& Other )
      : m_X( Other.m_X )
      , m_Y( Other.m_Y )
      , m_Z( Other.m_Z )
    {
        Normalizer::Perform( *this );
    }

Pretty sneaky huh? That will create a copy constructor for the normalized vector that accepts a plain vector, and vice versa. Pretty smooth…. only one problem, it won’t compile. Did you already figure out why? Feeling smart enough to guess? I’m gonna tell you in the next sentence to you better hurry up. It won’t compile because Vector
is not allowed to see the private members of Vector. Remember, they’re completely different objects. However the fix is pretty trivial. As my old professor used to say “only friends can see your private parts.” You just need to expose each type of vector to the other. You can use the same trick you used with the copy constructor:

    friend class Vector;

And that’s the end of that story. I now have one body of code that compiles cleanly into a normalizing and non-normalizing vector. This leaves one huge question in my mind, was this a good thing? If any of you have experience with this kind of thing I would LOVE to hear your feedback. Maybe when I get to the second half of chapter 3 I’ll learn that I just committed the classic rookie mistake. I’ll let you know.

posted by james wells at 4:49 pm

No Comments »

No comments yet.

RSS feed for comments on this post. TrackBack URL

Leave a comment