Preprocessor metaprogramsSome of you might wonder how named tuples are implemented in Aurora. How can it be that the macro
AURORA_NAMED_TUPLE(MyTuple, ((int, i), (float, f)))
generates the following code?
struct MyTuple
{
MyTuple(int i, float f) : i(i), f(f) {}
int i;
float f;
}
The answer leads to a dark corner of C and C++: Preprocessor metaprogramming. In general, macros have a bad reputation, and mostly they can be avoided by using other abstraction techniques such as templates. However, legitimate use cases remain; templates and even template metaprogramming don't cover every situation. For example,
a previous post of mine shows the drawbacks of
std::tuple, namely its lack of expressiveness.
Most people are not aware that the C preprocessor is not only useful for simple text processing, but it has the capabilities of code generation -- before compile time, and within the language itself. As such, it is very powerful in specific situations where the only alternative would be manual and error-prone code repetition. These situations are rare, but when you encounter them, the preprocessor can prove invaluable.
Aurora provides
a minimalistic preprocessor metaprogramming toolbox. While inspired from Boost.Preprocessor, some functionality is tweaked for specific needs, and some parts are simplified for the user (e.g. by the use of variadic macros). To show you how easy and powerful it is, let's define an enum which allows conversion to strings.
#include <Aurora/Meta/Preprocessor.hpp>
#define ENUMERATOR(value, index) value,
#define TO_STRING(value, index) case value: return AURORA_PP_STRINGIZE(value);
#define SMART_ENUM(Enum, sequence) \
enum Enum \
{ \
AURORA_PP_FOREACH(ENUMERATOR, sequence) \
}; \
\
const char* toString(Enum e) \
{ \
switch (e) \
{ \
AURORA_PP_FOREACH(TO_STRING, sequence) \
} \
} \
Once we have defined the preprocessor metafunction
SMART_ENUM, we can use it to generate code.
SMART_ENUM(Color, (Red, Blue, Green))
expands to:
enum Color
{
Red,
Blue,
Green,
};
const char* toString(Color e)
{
switch (e)
{
case Red: return "Red";
case Blue: return "Blue";
case Green: return "Green";
}
}
And so, we can easily convert enumerators to strings.
int main()
{
Color c = Green;
const char* s = toString(c); // s is "Green"
}
Since the preprocessor doesn't allow iteration or recursion, metaprogramming helpers are implemented by means of manual repetition, which limits the number of processed elements. At the moment, sequences may not be longer than 5 elements, but this can be extended.
What do you think about named tuples and preprocessor metaprograms? Even if you don't need them immediately, can you imagine their utility?