5

I am using x-macros to reduce the amount of repetition and code duplication while implementing a Lua interface for the game Bitfighter. The following code works fine:

  //                            Fn name     Valid param profiles  Profile count                           
#  define TELEPORTER_LUA_METHOD_TABLE \
      TELEPORTER_LUA_METHOD_ITEM(addDest,    ARRAYDEF({{ PT,  END }}), 1 ) \
      TELEPORTER_LUA_METHOD_ITEM(delDest,    ARRAYDEF({{ INT, END }}), 1 ) \
      TELEPORTER_LUA_METHOD_ITEM(clearDests, ARRAYDEF({{      END }}), 1 ) \


// BLOCK A Start
const luaL_reg Teleporter::luaMethods[] =
{
#  define TELEPORTER_LUA_METHOD_ITEM(name, b, c) { #name, luaW_doMethod<Teleporter, &Teleporter::name > },
      TELEPORTER_LUA_METHOD_TABLE
#  undef TELEPORTER_LUA_METHOD_ITEM
   { NULL, NULL }
};
// BLOCK A End

  /* Generates the following:
  const luaL_reg Teleporter::luaMethods[] =
  {
       { "addDest", luaW_doMethod<Teleporter, &Teleporter::addDest > }
       { "delDest", luaW_doMethod<Teleporter, &Teleporter::delDest > }
       { "clearDests", luaW_doMethod<Teleporter, &Teleporter::clearDests > }
       { NULL, NULL }
  };
  */


// BLOCK B Start
const LuaFunctionProfile Teleporter::functionArgs[] =
{
#  define TELEPORTER_LUA_METHOD_ITEM(name, profiles, profileCount) { #name, profiles, profileCount },
      TELEPORTER_LUA_METHOD_TABLE
#  undef TELEPORTER_LUA_METHOD_ITEM
   { NULL, { }, 0 }
};
// BLOCK B End


  /* Generates the following:
  const LuaFunctionProfile Teleporter::functionArgs[] =
  {
     { "addDest",    {{ PT,  END }}, 1 }
     { "delDest",    {{ INT, END }}, 1 }
     { "clearDests", {{      END }}, 1 }
     { NULL, { }, 0 }
  };
  */

#undef TELEPORTER_LUA_METHOD_TABLE

So far, so good.

Except that I do essentially the same thing in dozens of classes. What I'd REALLY like to do is define the method table in each class (which can be called anything), then define two macros that can be called like this:

GENERATE_LUA_METHODS(Teleporter, TELEPORTER_LUA_METHOD_TABLE)
GENERATE_FUNCTION_PROFILE(Teleporter, TELEPORTER_LUA_METHOD_TABLE)

to avoid all the repeated code above in blocks A & B. The obvious way is to use a nested macro, but that is, unfortunately, illegal.

Is there a better way?


SOLUTION


When I posted this question, I was pretty sure the answer would be "can't be done." Instead, I got two approaches, one of which was exactly what I was looking for. There was also a good discussion of the pitfalls of macros (there are many), with some alternative approaches suggested. The implementation I developed based on the accepted answer is clean and easy to understand, with the dirty macro stuff conveniently out-of-sight.

In a hidey-hole somewhere:

#define ARRAYDEF(...) __VA_ARGS__   // Don't confuse the preprocessor with array defs


////////////////////////////////////////
////////////////////////////////////////
//
// Some ugly macro defs that will make our Lua classes sleek and beautiful
//
////////////////////////////////////////
////////////////////////////////////////
//
// See discussion of this code here:
// http://stackoverflow.com/questions/11413663/reducing-code-repetition-in-c
//
// Start with a definition like the following:
// #define LUA_METHODS(CLASS, METHOD) \
//    METHOD(CLASS, addDest,    ARRAYDEF({{ PT,  END }}), 1 ) \
//    METHOD(CLASS, delDest,    ARRAYDEF({{ INT, END }}), 1 ) \
//    METHOD(CLASS, clearDests, ARRAYDEF({{      END }}), 1 ) \
//

#define LUA_METHOD_ITEM(class_, name, b, c) \
  { #name, luaW_doMethod<class_, &class_::name > },

#define GENERATE_LUA_METHODS_TABLE(class_, table_) \
  const luaL_reg class_::luaMethods[] =            \
  {                                                \
    table_(class_, LUA_METHOD_ITEM)                \
    { NULL, NULL }                                 \
  }

// Generates something like the following:
// const luaL_reg Teleporter::luaMethods[] =
// {
//       { "addDest",    luaW_doMethod<Teleporter, &Teleporter::addDest >    }
//       { "delDest",    luaW_doMethod<Teleporter, &Teleporter::delDest >    }
//       { "clearDests", luaW_doMethod<Teleporter, &Teleporter::clearDests > }
//       { NULL, NULL }
// };

////////////////////////////////////////

#define LUA_FUNARGS_ITEM(class_, name, profiles, profileCount) \
  { #name, profiles, profileCount },

#define GENERATE_LUA_FUNARGS_TABLE(class_, table_)  \
  const LuaFunctionProfile class_::functionArgs[] = \
  {                                                 \
    table_(class_, LUA_FUNARGS_ITEM)                \
    { NULL, { }, 0 }                                \
  }

// Generates something like the following:
// const LuaFunctionProfile Teleporter::functionArgs[] =
// {
//    { "addDest",    {{ PT,  END }}, 1 }
//    { "delDest",    {{ INT, END }}, 1 }
//    { "clearDests", {{      END }}, 1 }
//    { NULL, { }, 0 }
// };

////////////////////////////////////////
////////////////////////////////////////

In each class file:

//               Fn name     Param profiles       Profile count                           
#define LUA_METHODS(CLASS, METHOD) \
   METHOD(CLASS, addDest,    ARRAYDEF({{ PT,  END }}), 1 ) \
   METHOD(CLASS, delDest,    ARRAYDEF({{ INT, END }}), 1 ) \
   METHOD(CLASS, clearDests, ARRAYDEF({{      END }}), 1 ) \

GENERATE_LUA_METHODS_TABLE(Teleporter, LUA_METHODS);
GENERATE_LUA_FUNARGS_TABLE(Teleporter, LUA_METHODS);

#undef LUA_METHODS
Brian Tompsett - 汤莱恩
  • 5,753
  • 72
  • 57
  • 129
Watusimoto
  • 1,773
  • 1
  • 23
  • 38
  • 4
    I am hugely in favour of generating large chunks of boilerplate code using a script. You are effectively creating your own DSL here anyway, using macros which can be unpleasant to debug and comprehend. C macros are not particularly expressive, and writing a code generator is likely to be easier and the resulting code is likely to be much more readable and debuggable. – Rook Jul 10 '12 at 13:22
  • I agree with the thrust of your comment, and share your dislike of C macros. However, this has to compile on a variety of different platforms, and using an external tool to generate code will likely make the build process overly cumbersome. – Watusimoto Jul 10 '12 at 13:30
  • 2
    I've used (or written) code generators ranging from simple awk scripts, to python, to C programs that have to be compiled first. They never made the build process _overly cumbersome_ - you write the rule once, and then just run `make` (or whatever). – Useless Jul 10 '12 at 13:42
  • @Watusimoto: integrating (say) a perl script into a build system is nowhere near as unpleasant as having to craft your own complex macro system. I've done exactly this in visual studio, maven and make for various projects. It you felt particularly masochistic, you could write the code generator in C++ so you only had to manage a single language, and it would _still_ be more manageable than the sort of macro monstrosity you're putting together. – Rook Jul 10 '12 at 14:03
  • We currently support gcc (with make), VC++, and XCode. One of our project goals is to reduce barriers to people playing with the code, so the fewer tools required to build, the better. That said, the code also needs to be intelligible, so there is a clear tradeoff. At the moment, I don't foresee things getting much more complex than what I've presented here, so the built-in preprocessor seems the least bad option. If things become more complex, that calculus may change, and we might want to support a more elaborate (and understandable) preprocessing system. – Watusimoto Jul 10 '12 at 14:50
  • @Watusimoto: `perl` might be a bit complicated, however Python is intelligible and cross-platform. Otherwise, there is always the `tablegen` alternative used by LLVM and Clang. Given the liberal license of the project, you could extract `tablegen` from it and enjoy... there has already been talk about making it more generic so it could be reused by others. – Matthieu M. Jul 10 '12 at 14:58
  • Code that's littered with unintellegible macros is certainly a bigger barrier for people who'd like to play with the code, than having to have Perl or Python. Many of the people who are likely to go playing around with C++ and Lua code will probably have those installed already, anyway. – leftaroundabout Jul 10 '12 at 20:03
  • @leftaroundabout: I think it is less clear-cut than you present it to be. To implement a Python solution, we'd still need to have something that looks like the "in class file" block in the solution part of my question, i.e. a block of text that was not C++ code. Except that it would be expanded via Python rather than the preprocessor. Perhaps the expansion code itself would be easier to understand, but it would be off in an external file somewhere, with no IDE support, rendering it unintelligible to the casual reader. It is not clear to me that Python would automatically improve the situation. – Watusimoto Jul 10 '12 at 23:00

2 Answers2

2

functional approach may solve many of your woes, but you should be aware that heavy use of preprocessor leads to code that is hard to debug. you're bound to spend much time formatting the code whenever there's a syntax error in the generated code (and you're bound to hit that when your macro use grows sufficiently); it'll also impact your mood when you need to use gdb or some such.

the following is obviously just a sketch to give you an idea.

#  define TELEPORTER_LUA_METHOD_TABLE(class_, item) \
      item(class_, addDest,    ARRAYDEF({{ PT,  END }}), 1 ) \
      item(class_, delDest,    ARRAYDEF({{ INT, END }}), 1 ) \
      item(class_, clearDests, ARRAYDEF({{      END }}), 1 ) \

#  define LUA_METHOD_ITEM(class_, name, b, c) \
  { #name, luaW_doMethod<class_, &class_::name > },

#  define LUA_FUNARGS_ITEM(class_, name, profiles, profileCount) \
  { #name, profiles, profileCount },

#define LUA_METHODS_TABLE(class_, table) \
  const luaL_reg class_::luaMethods[] = \
  { \
    table(class_, LUA_METHOD_ITEM) \
    { NULL, NULL } \
  };

#define LUA_FUNARGS_TABLE(class_, table) \
  const LuaFunctionProfile class_::functionArgs[] = \
  { \
    table(class_, LUA_FUNARGS_ITEM) \
    { NULL, { }, 0 } \
  };

LUA_METHODS_TABLE(Teleporter, TELEPORTER_LUA_METHOD_TABLE)

LUA_FUNARGS_TABLE(Teleporter, TELEPORTER_LUA_METHOD_TABLE)

#undef TELEPORTER_LUA_METHOD_TABLE

edit to answer Watusimoto's question from the comments.

Watusimoto proposes something like this:

#define LUA_METHODS_TABLE(class_) \
  const luaL_reg class_::luaMethods[] = \
  { \
    LUA_METHOD_TABLE(class_, LUA_METHOD_ITEM) \
    { NULL, NULL } \
  };

#define LUA_FUNARGS_TABLE(class_, table) \
  const LuaFunctionProfile class_::functionArgs[] = \
  { \
    LUA_METHOD_TABLE(class_, LUA_FUNARGS_ITEM) \
    { NULL, { }, 0 } \
  };


#ifdef LUA_METHOD_TABLE
# undef LUA_METHOD_TABLE
#endif

#  define LUA_METHOD_TABLE(class_, item) \
      ... class-specific definition ...

LUA_METHODS_TABLE(Teleporter)
LUA_FUNARGS_TABLE(Teleporter)

The drawback to this is that it's not clear how is LUA_METHOD_TABLE related to the two macro calls that follow. It's just as if sprintf(3) didn't take arguments and instead expected the data in global variables of specific names. From the comprehensibility point of view it's better for any piece of code to be explicit about its immediate inputs, things it works on and which are different between its uses. But the global table macro also loses on the composability front: the global macro precludes production of multiple class definitions in one go, eg. with BPP or similar.

just somebody
  • 18,602
  • 6
  • 51
  • 60
  • On a first read, this looks very good. By standardizing the name of the table, we can eliminate the 2nd parameter in the final two macros, leaving: LUA_METHODS_TABLE(Teleporter); LUA_FUNARGS_TABLE(Teleporter);, which is very nice indeed. – Watusimoto Jul 10 '12 at 14:33
  • @Watusimoto: If you are going to play with the preprocessor, I recommend looking at Boost.Preprocessor. Not only do they have *LOTS* of macros for iterating and stuff, but they also demonstrate what can be accomplished and **how**. – Matthieu M. Jul 10 '12 at 15:00
  • @Watusimoto: i wouldn't do that. you'd have to `#undef` the table before every use; referential transparency is good. – just somebody Jul 10 '12 at 15:04
  • I'm not sure I follow -- in the code you posted above, you #undef the table after using it. Wouldn't that be enough? – Watusimoto Jul 10 '12 at 15:48
  • @Watusimoto: i've updated the answer, please edit it if i misrepresented your position. – just somebody Jul 10 '12 at 16:29
  • It's a fair representation, and while I am not 100% convinced, I think on balance you are correct. – Watusimoto Jul 10 '12 at 19:12
1

This is some rather extreme preprocessor hackery, but you can do this with a few different files.

teleporter.cpp:

#define LMT_CLASS_NAME Teleporter
#define LMT_TABLE_FILE "Teleporter.lmt"
#include "lua_method_table.h"

lua_method_table.h:

#define LMT_METHOD_ITEM(name, b, c) { #name, luaW_doMethod<LMT_CLASS_NAME, &LMT_CLASS_NAME::name > },

const luaL_reg LMT_CLASS_NAME::luaMethods[] = 
    #include LMT_TABLE_FILE
    { NULL, NULL }
};

#undef LMT_METHOD_ITEM

#define LMT_METHOD_ITEM(name, profiles, profileCount) { #name, profiles, profileCount },

const LuaFunctionProfile LMT_CLASS_NAME::functionArgs[] =
{
    #include LMT_TABLE_FILE
    { NULL, { }, 0 }
}; 

#undef LMT_METHOD_ITEM

And finally teleporter.lmt:

LMT_METHOD_ITEM(addDest,    ARRAYDEF({{ PT,  END }}), 1 )
LMT_METHOD_ITEM(delDest,    ARRAYDEF({{ INT, END }}), 1 ) 
LMT_METHOD_ITEM(clearDests, ARRAYDEF({{      END }}), 1 ) 

Instead of using a macro to define the method table, it is listed in a file, teleporter.lmt, which is included twice with different definitions of LMT_METHOD_ITEM. There's no header guards in there so it can be included as many times as necessary. If you want, you could split lua_method_table.h into two files to handle the two parts separately. Just include both of them from your CPP files.

IronMensan
  • 6,761
  • 1
  • 26
  • 35