|
Using The CPP For Metaprogramming
Submitted by |
The C preprocessor is dreaded by many programmers. Despite the limitations
of the CPP, it can be used for simple metaprogramming, which can save a lot
of typing. Unless you can eat while typing, like some programmers do, you
might enjoy to add this technique to your bag of tricks.
Please note that most code in this article has been written directly into
this article and has never passed through a compiler. While I'm pretty good
at spotting typing errors in C++ code, most compilers are still much better
than me.
Generating program components using macros
Suppose you are writing a vector or an arithmetic array class and you want
to implement all kinds of component-wise operators. The + and - operators
could look like this:
template<class T, int n
Vec<T,n operator+(const Vec<T,n& lhs, const Vec<T,n& rhs)
{
Vec<T,n res;
for (int i=0; i<n; ++i)
res[i] = lhs[i] + rhs[i];
return res;
}
template<class T, int n
Vec<T,n operator-(const Vec<T,n& lhs, const Vec<T,n& rhs)
{
Vec<T,n res;
for (int i=0; i<n; ++i)
res[i] = lhs[i] - rhs[i];
return res;
} |
As you can see there is hardly any difference between the two routines.
Indeed, if you would print the routines on transparent film and put them on
top of each other, you would only see the first version. Essentially the
two routines are token for token identical, except for the operators + and -.
But you are not through yet, in addition to the above two operators, you
would have to write the same kind of code for *,/,%,&,^,|, etc... depending
on your requirements. You can probably imagine that the amount of code
would get quickly out of hand. Isn't there a more concise way to express this?
Well, with some simple generic programming, we could factor out the loop,
but that is not the point of this article. Unfortunately, the C++ template
mechanism just isn't powerful enough for what we are trying to do. The CPP,
however, does not have that limitation. What we can do is to create a
macro, parameterized by the variable parts of the functions, and then use
that macro to generate the functions:
#define UMP_DEF_VEC_OP(op) \
template<class T, int n \
Vec<T,n operator op(const Vec<T,n& lhs, const Vec<T,n& rhs)\
{ \
Vec<T,n res; \
\
for (int i=0; i<n; ++i) \
res[i] = lhs[i] op rhs[i];\
\
return res; \
}
UMP_DEF_VEC_OP(*)
UMP_DEF_VEC_OP(/)
UMP_DEF_VEC_OP(%)
UMP_DEF_VEC_OP(+)
UMP_DEF_VEC_OP(-)
UMP_DEF_VEC_OP(<<)
UMP_DEF_VEC_OP()
UMP_DEF_VEC_OP(&)
UMP_DEF_VEC_OP(^)
UMP_DEF_VEC_OP(|)
#undef UMP_DEF_VEC_OP |
This example can be taken a step further by defining the corresponding
compound assignment and scalar operators as well:
#define UMP_DEF_VEC_OP(op) \
template<class T, int n \
Vec<T,n& operator op##=(Vec<T,n& lhs, const Vec<T,n& rhs)\
{ \
for (int i=0; i<n; ++i) \
lhs[i] op##= rhs[i]; \
\
return lhs; \
} \
\
template<class T, int n \
Vec<T,n operator op(const Vec<T,n& lhs, const Vec<T,n& rhs)\
{ \
Vec<T,n res; \
\
for (int i=0; i<n; ++i) \
res[i] = lhs[i] op rhs[i];\
\
return res; \
} \
\
template<class T, int n \
Vec<T, n operator op(const Vec<T,n& lhs, T rhs)\
{ \
Vec<T,n res; \
\
for (int i=0; i<n; ++i) \
res[i] = lhs[i] op rhs; \
\
return res; \
} \
\
template<class T, int n \
Vec<T, n operator op(T lhs, const Vec<T,n& rhs)\
{ \
Vec<T,n res; \
\
for (int i=0; i<n; ++i) \
res[i] = lhs op rhs[i]; \
\
return res; \
}
UMP_DEF_VEC_OP(*)
UMP_DEF_VEC_OP(/)
UMP_DEF_VEC_OP(%)
UMP_DEF_VEC_OP(+)
UMP_DEF_VEC_OP(-)
UMP_DEF_VEC_OP(<<)
UMP_DEF_VEC_OP()
UMP_DEF_VEC_OP(&)
UMP_DEF_VEC_OP(^)
UMP_DEF_VEC_OP(|)
#undef UMP_DEF_VEC_OP |
Hopefully you are starting to see that this kind of metaprogramming can
save you a LOT of typing. Not only does it save you typing, it also
makes it more difficult to make errors, because it is much easier to spot
an error when you only have to check the macro parameters without having to
browse through huge amounts of repeated code.
Here is an algorithm for writing the above kind of macro:
1. Write the complete definition of the kind of program element you wish to
generate. This way it is easy to get it right.
2. Select the code you just wrote and replace each occurance of the token
you wish to generate with a parameter token. You may need to use the token
pasting operator ## (see your C/C++ manual).
3. Write the macro definition. Remember that preprocessor statements must
begin at the start of line in standard C++. Remember to use the line
continuation character on every line except the last.
4. Write the macro undefinition. This is important, because otherwise you
will quickly litter the global namespace (*).
5. Use the macro to generate the program elements you wanted.
6. Take a moment to reflect upon your work, then ask your boss for a raise,
because you are now working n-times faster.
(*) Technically, macros are not related to namespaces.
From O(n**2) to O(1) typing
The above example of generating the vector operators is just one example of
how you can take advantage of the CPP. I will present one more technique,
which can prove useful sometimes.
The CPP token pasting operator can be used to define tokens based on macro
parameters. This way you can generate finite lists neatly using the CPP.
For example, with suitable definitions, the following code
#define UMP_LIST_ELEM(i) i
#define UMP_LIST_SEP(i) ,
int numbers[] = {UMP_LIST(10, UMP_LIST_SEP, UMP_LIST_ELEM)};
#undef UMP_LIST_SEP
#undef UMP_LIST_ELEM |
would generate the following code:
int numbers[] = {0,1,2,3,4,5,6,7,8,9}; |
Now, what you just saw probably doesn't rock your balls, but I'll give a
stiffer example. Let's get back to writing the vector class. Suppose we
want to write suitable constructors for our generic vector class:
template<class T, int n
class Vec {
T m_v;
public:
Vec()
{}
Vec(T t_0)
{m_v[0] = t_0;}
Vec(T t_0, T t_1)
{m_v[0] = t0; m_v[1] = t_1;}
Vec(T t_0, T t_1, T t_2)
{m_v[0] = t_0; m_v[1] = t_1; m_v[2] = t_2;}
Vec(T t_0, T t_1, T t_2, T t_3)
{m_v[0] = t_0; m_v[1] = t_1; m_v[2] = t_2; m_v[3] = t_3;}
// And so on...
}; |
Writing the constructor definitions involves typing O(n**2) tokens, where n
is the amount of arguments you want for the biggest constructor. That is a
LOT of typing! Fortunately we can use the CPP to generate this stuff
for us using essentially O(1) tokens. Actually I'm cheating a bit, because
I'm assuming that we have previously written the reusable preprocessor
helpers that I now present:
#define UMP_LIST(n, SEP, ELEM) UMP_LIST_##n(SEP, ELEM)
#define UMP_LIST_0(SEP, ELEM)
#define UMP_LIST_1(SEP, ELEM) ELEM(0)
#define UMP_LIST_2(SEP, ELEM) UMP_LIST_1(SEP, ELEM) SEP(1) ELEM(1)
#define UMP_LIST_3(SEP, ELEM) UMP_LIST_2(SEP, ELEM) SEP(2) ELEM(2)
#define UMP_LIST_4(SEP, ELEM) UMP_LIST_3(SEP, ELEM) SEP(3) ELEM(3)
#define UMP_LIST_5(SEP, ELEM) UMP_LIST_4(SEP, ELEM) SEP(4) ELEM(4)
#define UMP_LIST_6(SEP, ELEM) UMP_LIST_5(SEP, ELEM) SEP(5) ELEM(5)
#define UMP_LIST_7(SEP, ELEM) UMP_LIST_6(SEP, ELEM) SEP(6) ELEM(6)
#define UMP_LIST_8(SEP, ELEM) UMP_LIST_7(SEP, ELEM) SEP(7) ELEM(7)
#define UMP_LIST_9(SEP, ELEM) UMP_LIST_8(SEP, ELEM) SEP(8) ELEM(8)
#define UMP_LIST_10(SEP, ELEM) UMP_LIST_9(SEP, ELEM) SEP(9) ELEM(9)
// ...
#define UMP_LST2(n, SEP, ELEM) UMP_LST2_##n(SEP, ELEM)
#define UMP_LST2_0(SEP, ELEM)
#define UMP_LST2_1(SEP, ELEM) ELEM(0)
#define UMP_LST2_2(SEP, ELEM) UMP_LST2_1(SEP, ELEM) SEP(1) ELEM(1)
#define UMP_LST2_3(SEP, ELEM) UMP_LST2_2(SEP, ELEM) SEP(2) ELEM(2)
#define UMP_LST2_4(SEP, ELEM) UMP_LST2_3(SEP, ELEM) SEP(3) ELEM(3)
#define UMP_LST2_5(SEP, ELEM) UMP_LST2_4(SEP, ELEM) SEP(4) ELEM(4)
#define UMP_LST2_6(SEP, ELEM) UMP_LST2_5(SEP, ELEM) SEP(5) ELEM(5)
#define UMP_LST2_7(SEP, ELEM) UMP_LST2_6(SEP, ELEM) SEP(6) ELEM(6)
#define UMP_LST2_8(SEP, ELEM) UMP_LST2_7(SEP, ELEM) SEP(7) ELEM(7)
#define UMP_LST2_9(SEP, ELEM) UMP_LST2_8(SEP, ELEM) SEP(8) ELEM(8)
#define UMP_LST2_10(SEP, ELEM) UMP_LST2_9(SEP, ELEM) SEP(9) ELEM(9)
// ... |
The repetition (and the second list) is required, because the CPP does not
allow recursion. Now we are ready to write the constructors:
template<class T, int n
class Vec {
T m_v;
public:
#define UMP_ARG_SEP(i) ,
#define UMP_ARG_ELEM(i) T t_##i
#define UMP_INIT_ELEM(i) m_v[i] = t_##i;
#define UMP_EMPTY(i)
#define UMP_DEF_CTOR(n) \
Vec(UMP_LIST(n, UMP_ARG_SEP, UMP_ARG_ELEM))\
{UMP_LIST(n, UMP_EMPTY, UMP_INIT_ELEM)}
UMP_LST2(10, UMP_EMPTY, UMP_DEF_CTOR)
#undef UMP_ARG_SEP
#undef UMP_ARG_ELEM
#undef UMP_INIT_ELEM
#undef UMP_EMPTY
}; |
As an exercise, try writing the constructors by hand for upto something
like 20 arguments. You'll soon find that it takes plenty of time. Then try
using the previous technique. You should notice that the amount of code
generating the constructors stays the same. Only the reusable CPP list
generation code needs to be extended upto the desired length.
Obviously you don't have to go this far to get benefits from using the CPP,
but you don't have to stop here either. And don't you dare take this as a
permit to use the CPP to define constants!
Regards,
Vesa Karvonen
Housemarque, Inc.
|
The zip file viewer built into the Developer Toolbox made use
of the zlib library, as well as the zlibdll source additions.
|