Keeping Lists in Sync in C++

2004-08-17

Here's a small idea that you might find useful for your C++ code. I learned this while working at Sega of Japan.

In a lot of programming and especially in games we need lots of tables. Of the those tables need to have constants to access them or there may be parallel tables that need to stay in sync. Having a list of constants (defines or enums) and also having one or more tables those constants have to stay in sync with is a real pain in the ass. On top of that almost every programmer that's used them has run into problems where one of their tables was out of sync and it took time, could even be hours, to track down the bug only to finally figure out they were out of sync.

So, a solution is needed. Back in the 8 bit days we could not even do structures so all data was stored in parallel arrays. For example all enemies might need hit points, damage, speed etc. All that in today's world would be stored in an array of structures but back then it was much more efficient to store them in parallel arrays. Unfortunately, trying to find the 49th line in each array to edit it was massively slow and error prone so I wrote a tool to take an structure like array and break it into parallel arrays.

Once we moved to structures that problem disappeared but using constants like PLAYER=1 and BUGEYEDMONSTER=47 and keeping those in sync with your tables was still a problem. And, other times you'd still need more than one table and all of it needed to stay in sync. Add something to one table and forget to update another and crash!

For that purpose my friend John Alvarado wrote a program called definer. It will take that kind of data and write out multiple files like a .h file with your constants and a .cpp file with your table, even a .inc file for your assembly language and anything else you might need as well.

That's great and it has it's place but sometimes it's overkill if you just have a small localized problem to solve.

This trick I learned at Sega involves using a macro to define your constants and tables and enclosing each item in the list in an undefined macro. Then, anytime you need to you can generate a table and use the info you need. Since there is only 1 place to edit everything (the table macro) you never have to worry about things getting out of sync. Less work, less error prone, everybody wins.

To give you an example, here's some sample code you might see in a game to startup the various characters in a game. Somewhere you would have or load an array of data that lists the types of objects you need to appear and where they appear and then you'd walk the list and generate all the objects maybe something like this:

/*  ================ normal ==========================  */

#include "limits.h"

struct VECTOR3
{
    float x;
    float y;
    float z;
};

/**************************** enums for types ****************************/

class GameTypeID
{
public:
    enum ID
    {
        Player,
        Ogre,
        Orc,

        last,
        force_int = INT_MAX,
    };
};

/****************** one entry in a table of game object ******************/
/******************  to introduce when the level starts ******************/

struct IntroData
{
    GameTypeID::ID  gameType;   // they type of object
    VECTOR3         position;   // where it starts
    VECTOR3         rotation;   // the way it faces
};

/************************ base class game object *************************/

class CGameObject
{
};

/******************************** Player *********************************/

class CPlayer : public CGameObject
{
};

/********************************* Ogre **********************************/

class COgre : public CGameObject
{
};

/********************************** Orc **********************************/

class COrc : public CGameObject
{
};

/***********************************  ************************************/
// some function that would take are new object and add it to our
// game system setting the object's position, orientation and other stuff

extern void AddObjectToSystem (CGameObject* pOb, IntroData* pIntro);

/******************************* InitLevel *******************************/
// takes an array of IntroData and
// starts up all the objects in that list

void InitLevel (IntroData* pData, int numObjects)
{
    while (numObjects)
    {
        CGameObject*    pNewOb = NULL;

        switch (pData->gameType)
        {
        case GameTypeID:😛layer:
            pNewOb = new CPlayer;
            printf ("made player\n");
            break;
        case GameTypeID::Ogre:
            pNewOb = new COgre;
            printf ("made ogre\n");
            break;
        case GameTypeID::Orc:
            pNewOb = new COrc;
            printf ("made orc\n");
            break;
        default:
            printf ("error: unknown type\n");
            break;
        }

        if (pNewOb)
        {
            AddObjectToSystem (pNewOb, pData);
        }

        ++pData;
        --numObjects;
    }
}

Maybe that was a bad example since there is not much to keep in sync there 😞 But dang it, I already wrote it so we'll go with it. You can see that every time we add a new type of GameObject we have to edit the case statement in InitLevel() to match and we have to update the GameTypeID enum with a new type. Also there is a case statement which might be slow (it might have to check every case) and there is lots of redundant code (the assignment to pNewOb and the 4 printfs)

Here's the same example using the define macro list trick

/*  ================ first attempt ==========================  */

#include "limits.h"

struct VECTOR3
{
    float x;
    float y;
    float z;
};

/***************************** list of types *****************************/

#define GAMETYPE_LIST(OP)   \
    OP(Player)              \
    OP(Ogre)                \
    OP(Orc)                 \

/**************************** enums for types ****************************/

class GameTypeID
{
public:
    enum ID
    {
        #define GAMETYPE_OP(name) name,
        GAMETYPE_LIST(GAMETYPE_OP)
        #undef GAMETYPE_OP
        last,
        force_int = INT_MAX,
    };
};

/****************** one entry in a table of game object ******************/
/******************  to introduce when the level starts ******************/

struct IntroData
{
    GameTypeID::ID  gameType;   // they type of object
    VECTOR3         position;   // where it starts
    VECTOR3         rotation;   // the way it faces
};

/************************ base class game object *************************/

class CGameObject
{
};

/******************************** Player *********************************/

class CPlayer : public CGameObject
{
};

/********************************* Ogre **********************************/

class COgre : public CGameObject
{
};

/********************************** Orc **********************************/

class COrc : public CGameObject
{
};

/***********************************  ************************************/
// some function that would take are new object and add it to our
// game system setting the object's position, orientation and other stuff

extern void AddObjectToSystem (CGameObject* pOb, IntroData* pIntro);

/******************************* InitLevel *******************************/
// takes an array of IntroData and
// starts up all the objects in that list

void InitLevel (IntroData* pData, int numObjects)
{
    while (numObjects)
    {
        CGameObject*    pNewOb = NULL;

        switch (pData->gameType)
        {
        #define GAMETYPE_OP(name)   \
                case GameTypeID::name:  \
                pNewOb = new C ## name; \
                printf ("made " #name  "\n");   \
                    break;  \

        GAMETYPE_LIST(GAMETYPE_OP)
        #undef GAMETYPE_OP
        default:
            printf ("error: unknown type\n");
            break;
        }

        if (pNewOb)
        {
            AddObjectToSystem (pNewOb, pData);
        }

        ++pData;
        --numObjects;
    }
}

As you can see we make a list called GAMETYPE_LIST and generate both the enum and the case code. That saved us at least one place, we no longer have to edit the case code but it's still going to be a lot of code when we get to hundreds of objects.

Let's optimize a little. Here's what I would probably do now−a−days

/*  ================== best? ========================  */

#include "limits.h"

struct VECTOR3
{
    float x;
    float y;
    float z;
};

/***************************** list of types *****************************/

#define GAMETYPE_LIST(OP)   \
    OP(Player)              \
    OP(Ogre)                \
    OP(Orc)                 \

/**************************** enums for types ****************************/

class GameTypeID
{
public:
    enum ID
    {
        #define GAMETYPE_OP(name) name,
        GAMETYPE_LIST(GAMETYPE_OP)
        #undef GAMETYPE_OP
        last,
        force_int = INT_MAX,
    };
};

/****************** one entry in a table of game object ******************/
/******************  to introduce when the level starts ******************/

struct IntroData
{
    GameTypeID::ID  gameType;   // they type of object
    VECTOR3         position;   // where it starts
    VECTOR3         rotation;   // the way it faces
};

/************************ base class game object *************************/

class CGameObject
{
};

/******************************** Player *********************************/

class CPlayer : public CGameObject
{
public:
    static CGameObject* create ()
    {
        return new CPlayer;
    }
};

/********************************* Ogre **********************************/

class COgre : public CGameObject
{
public:
    static CGameObject* create ()
    {
        return new COgre;
    }
};

/********************************** Orc **********************************/

class COrc : public CGameObject
{
public:
    static CGameObject* create ()
    {
        return new COrc;
    }
};

/***********************************  ************************************/
// some function that would take are new object and add it to our
// game system setting the object's position, orientation and other stuff

extern void AddObjectToSystem (CGameObject* pOb, IntroData* pIntro);

/*************** a table of the function for making objects **************/

typedef CGameObject* (*GameObjectCreationFuncPtr)();
struct CreationData
{
    GameObjectCreationFuncPtr   func;
    const char*                 typeName;
};

CreationData CreationFuncTable[] =
{
    #define GAMETYPE_OP(name) { &C ## name::create, #name, },
    GAMETYPE_LIST(GAMETYPE_OP)
    #undef GAMETYPE_OP
};

/******************************* InitLevel *******************************/
// takes an array of IntroData and
// starts up all the objects in that list

void InitLevel (IntroData* pData, int numObjects)
{
    while (numObjects)
    {
        if (pData->gameType >= 0 &amp;&amp; pData->gameType < GameTypeID::last)
        {
            CGameObject*    pNewOb = NULL;

            CreationFuncTable[pData->gameType].func();

            printf ("made %s\n", CreationFuncTable[pData->gameType].typeName);

            AddObjectToSystem (pNewOb, pData);
        }
        else
        {
            printf ("ERROR: unknown game type\n");
        }

        ++pData;
        --numObjects;
    }
}

I gave each type a static (ie, global) function to create one of itself (you gotta do that in C++ since the internal vtable pointer for each new instance has to be initialized. Then, I instead of using the case statement I made a parallel array of pointers to functions to create those objects. That array is always in sync with the enums since they are both auto generated. The code has also gotten slightly simpler and smaller as there is only one printf now were as there used to be one per object. Also, the function table code will be faster and less code than a giant case statement.

That a good example and possibly where I would stop but in this particular example we can go overboard. 😛 You can see that each of the create() function is the same. Any type specific code could appear in that type's constructor so using the define macro list trick we can generate those functions as well. Here's that example.

/*  ============== overkill? ============================  */

#include "limits.h"

struct VECTOR3
{
    float x;
    float y;
    float z;
};

/***************************** list of types *****************************/

#define GAMETYPE_LIST(OP)   \
    OP(Player)              \
    OP(Ogre)                \
    OP(Orc)                 \

/**************************** enums for types ****************************/

class GameTypeID
{
public:
    enum ID
    {
        #define GAMETYPE_OP(name) name,
        GAMETYPE_LIST(GAMETYPE_OP)
        #undef GAMETYPE_OP
       last,
        force_int = INT_MAX,
    };
};

/****************** one entry in a table of game object ******************/
/******************  to introduce when the level starts ******************/

struct IntroData
{
    GameTypeID::ID  gameType;   // they type of object
    VECTOR3         position;   // where it starts
    VECTOR3         rotation;   // the way it faces
};

/************************ base class game object *************************/

class CGameObject
{
};

/******************************** Player *********************************/

class CPlayer : public CGameObject
{
public:
    static CGameObject* create ();
};

/********************************* Ogre **********************************/

class COgre : public CGameObject
{
public:
    static CGameObject* create ();
};

/********************************** Orc **********************************/

class COrc : public CGameObject
{
public:
    static CGameObject* create ();
};

/***********************************  ************************************/
// some function that would take are new object and add it to our
// game system setting the object's position, orientation and other stuff

extern void AddObjectToSystem (CGameObject* pOb, IntroData* pIntro);

/************************** creation functions ***************************/

#define GAMETYPE_OP(name) CGameObject* C ## name::create() { return new C ## name; }
GAMETYPE_LIST(GAMETYPE_OP)
#undef GAMETYPE_OP

/*************** a table of the function for making objects **************/

typedef CGameObject* (*GameObjectCreationFuncPtr)();
struct CreationData
{
    GameObjectCreationFuncPtr   func;
    const char*                 typeName;
};

CreationData CreationFuncTable[] =
{
    #define GAMETYPE_OP(name) { &amp;C ## name::create, #name, },
    GAMETYPE_LIST(GAMETYPE_OP)
    #undef GAMETYPE_OP
};

/******************************* InitLevel *******************************/
// takes an array of IntroData and
// starts up all the objects in that list

void InitLevel (IntroData* pData, int numObjects)
{
    while (numObjects)
    {
        if (pData->gameType >= 0 &amp;&amp; pData->gameType < GameTypeID::last)
        {
            CGameObject*    pNewOb = NULL;

            CreationFuncTable[pData->gameType].func();

            printf ("made %s\n", CreationFuncTable[pData->gameType].typeName);

            AddObjectToSystem (pNewOb, pData);
        }
        else
        {
            printf ("ERROR: unknown game type\n");
        }

        ++pData;
        --numObjects;
    }
}

Of course in that example the table was only the name of each type. If you needed more data in your table you just added to the macro and then update your _OP macros to match something like this

/***************************** list of types *****************************/

//     name     hp dmg, spd
#define GAMETYPE_LIST(OP)   \
    OP(Player, 100, 10, 15) \
    OP(Ogre,    50,  5, 20) \
    OP(Orc,     75,  8, 13) \

/**************************** enums for types ****************************/

class GameTypeID
{
public:
    enum ID
    {
        #define GAMETYPE_OP(name,hp,damage,speed) name,
        GAMETYPE_LIST(GAMETYPE_OP)
        #undef GAMETYPE_OP
        last,
        force_int = INT_MAX,
    };
};

/******************** a table of data for the objects ********************/

struct ObjectData
{
    GameObjectCreationFuncPtr   func;
    const char*                 typeName;
    const int                   startHitPoints;
    const int                   damage;
    const int                   speed;
};

ObjectData ObDataTable[] =
{
    #define GAMETYPE_OP(name,hp,damage,speed) \
       { &amp;C ## name::create, #name, hp, damage, speed, },
    GAMETYPE_LIST(GAMETYPE_OP)
    #undef GAMETYPE_OP
};

And there you have it. I've found it pretty useful. Of course there are times where I still need to use something like definer to keep things in sync across languages or tools but for small internal stuff this works well for me. 😊

Comments
Clickteam
Effective 3D Exporter Design: How to Make Artists Love You