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:
#include "limits.h"
struct VECTOR3
{
float x;
float y;
float z;
};
class GameTypeID
{
public:
enum ID
{
Player,
Ogre,
Orc,
last,
force_int = INT_MAX,
};
};
struct IntroData
{
GameTypeID::ID gameType; VECTOR3 position; VECTOR3 rotation;};
class CGameObject
{
};
class CPlayer : public CGameObject
{
};
class COgre : public CGameObject
{
};
class COrc : public CGameObject
{
};
extern void AddObjectToSystem (CGameObject* pOb, IntroData* pIntro);
void InitLevel (IntroData* pData, int numObjects)
{
while (numObjects)
{
CGameObject* pNewOb = NULL;
switch (pData->gameType)
{
case GameTypeID::Player:
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
#include "limits.h"
struct VECTOR3
{
float x;
float y;
float z;
};
#undef GAMETYPE_OB
#define GAMETYPE_LIST \
GAMETYPE_OP(Player) \
GAMETYPE_OP(Ogre) \
GAMETYPE_OP(Orc) \
class GameTypeID
{
public:
enum ID
{
#undef GAMETYPE_OP
#define GAMETYPE_OP(name) name,
GAMETYPE_LIST
last,
force_int = INT_MAX,
};
};
struct IntroData
{
GameTypeID::ID gameType; VECTOR3 position; VECTOR3 rotation;};
class CGameObject
{
};
class CPlayer : public CGameObject
{
};
class COgre : public CGameObject
{
};
class COrc : public CGameObject
{
};
extern void AddObjectToSystem (CGameObject* pOb, IntroData* pIntro);
void InitLevel (IntroData* pData, int numObjects)
{
while (numObjects)
{
CGameObject* pNewOb = NULL;
switch (pData->gameType)
{
#undef GAMETYPE_OP
#define GAMETYPE_OP(name) \
case GameTypeID::name: \
pNewOb = new C ## name; \
printf ("made " #name "\n"); \
break; \
GAMETYPE_LIST
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
#include "limits.h"
struct VECTOR3
{
float x;
float y;
float z;
};
#undef GAMETYPE_OB
#define GAMETYPE_LIST \
GAMETYPE_OP(Player) \
GAMETYPE_OP(Ogre) \
GAMETYPE_OP(Orc) \
class GameTypeID
{
public:
enum ID
{
#undef GAMETYPE_OP
#define GAMETYPE_OP(name) name,
GAMETYPE_LIST
last,
force_int = INT_MAX,
};
};
struct IntroData
{
GameTypeID::ID gameType; VECTOR3 position; VECTOR3 rotation;};
class CGameObject
{
};
class CPlayer : public CGameObject
{
public:
static CGameObject* create ()
{
return new CPlayer;
}
};
class COgre : public CGameObject
{
public:
static CGameObject* create ()
{
return new COgre;
}
};
class COrc : public CGameObject
{
public:
static CGameObject* create ()
{
return new COrc;
}
};
extern void AddObjectToSystem (CGameObject* pOb, IntroData* pIntro);
typedef CGameObject* (*GameObjectCreationFuncPtr)();
struct CreationData
{
GameObjectCreationFuncPtr func;
const char* typeName;
};
CreationData CreationFuncTable[] =
{
#undef GAMETYPE_OP
#define GAMETYPE_OP(name) { &C ## name::create, #name, },
GAMETYPE_LIST
};
void InitLevel (IntroData* pData, int numObjects)
{
while (numObjects)
{
if (pData->gameType >= 0 && 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.
#include "limits.h"
struct VECTOR3
{
float x;
float y;
float z;
};
#undef GAMETYPE_OB
#define GAMETYPE_LIST \
GAMETYPE_OP(Player) \
GAMETYPE_OP(Ogre) \
GAMETYPE_OP(Orc) \
class GameTypeID
{
public:
enum ID
{
#undef GAMETYPE_OP
#define GAMETYPE_OP(name) name,
GAMETYPE_LIST
last,
force_int = INT_MAX,
};
};
struct IntroData
{
GameTypeID::ID gameType; VECTOR3 position; VECTOR3 rotation;};
class CGameObject
{
};
class CPlayer : public CGameObject
{
public:
static CGameObject* create ();
};
class COgre : public CGameObject
{
public:
static CGameObject* create ();
};
class COrc : public CGameObject
{
public:
static CGameObject* create ();
};
extern void AddObjectToSystem (CGameObject* pOb, IntroData* pIntro);
#undef GAMETYPE_OP
#define GAMETYPE_OP(name) CGameObject* C ## name::create() { return new C ## name; }
GAMETYPE_LIST
typedef CGameObject* (*GameObjectCreationFuncPtr)();
struct CreationData
{
GameObjectCreationFuncPtr func;
const char* typeName;
};
CreationData CreationFuncTable[] =
{
#undef GAMETYPE_OP
#define GAMETYPE_OP(name) { &C ## name::create, #name, },
GAMETYPE_LIST
};
void InitLevel (IntroData* pData, int numObjects)
{
while (numObjects)
{
if (pData->gameType >= 0 && 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
#undef GAMETYPE_OB
#define GAMETYPE_LIST \
GAMETYPE_OP(Player, 100, 10, 15) \
GAMETYPE_OP(Ogre, 50, 5, 20) \
GAMETYPE_OP(Orc, 75, 8, 13) \
class GameTypeID
{
public:
enum ID
{
#undef GAMETYPE_OP
#define GAMETYPE_OP(name,hp,damage,speed) name,
GAMETYPE_LIST
last,
force_int = INT_MAX,
};
};
struct ObjectData
{
GameObjectCreationFuncPtr func;
const char* typeName;
const int startHitPoints;
const int damage;
const int speed;
};
ObjectData ObDataTable[] =
{
#undef GAMETYPE_OP
#define GAMETYPE_OP(name,hp,damage,speed) \
{ &C ## name::create, #name, hp, damage, speed, },
GAMETYPE_LIST
};
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. 
Very good article! Impressive use of macros! Makes me wish I'd have known that trick 6-7 years ago when I had to deal with a lot of C++ code containing long repetitive lists...!
I'll surely put this to use someday.