Andrea writes to confess some sins, though I'm not sure who the real sinner is. To understand the sins, we have to talk a little bit about C/C++ macros.
Andrea was working on some software to control a dot-matrix display from an embedded device. Send an array of bytes to it, and the correct bits on the display light up. Now, if you're building something like this, you want an easy way to "remember" the proper sequences. So you might want to do something like:
uint8_t glyph0[] = {'0', 0x0E, 0x11, 0x0E, 0};
uint8_t glyph1[] = {'1', 0x09, 0x1F, 0x01, 0};
And so on. And heck, you might want to go so far as to have a lookup array, so you might have a const uint8_t *const glyphs[] = {glyph0, glyph1…}
. Now, you could just hardcode those definitions, but wouldn't it be cool to use macros to automate that a bit, as your definitions might change?
Andrea went with a style known as X macros, which let you specify one pattern of data which can be re-used by redefining X. So, for example, I could do something like:
#define MY_ITEMS \
X(a, 5) \
X(b, 6) \
X(c, 7)
#define X(name, value) int name = value;
MY_ITEMS
#undef X
This would generate:
int a = 5;
int b = 6;
int c = 7;
But I could re-use this, later:
#define X(name, data) name,
int items[] = { MY_ITEMS nullptr};
#undef X
This would generate, in theory, something like: int items[] = {a,b,c,nullptr};
We are recycling the MY_ITEMS
macro, and we're changing its behavior by altering the X
macro that it invokes. This can, in practice, result in much more readable and maintainable code, especially code where you need to have parallel lists of items. It's also one of those things that the first time you see it, it's… surprising.
Now, this is all great, and it means that Andrea could potentially have a nice little macro system for defining arrays of bytes and a lookup array pointing to those arrays. There's just one problem.
Specifically, if you tried to write a macro like this:
#define GLYPH_DEFS \
X(glyph0, {'0', 0x0E, 0x11, 0x0E, 0})
It wouldn't work. It doesn't matter what you actually define X
to do, the preprocessor isn't aware of the C/C++ syntax. So it doesn't say "oh, that second comma is inside of an array initalizer, I'll ignore it", it says, "Oh, they're trying to pass more than two parameters to the macro X."
So, you need some way to define an array initializer that doesn't use commas. If macros got you into this situation, macros can get you right back out. Here is Andrea's solution:
#define _ , // Sorry.
#define GLYPH_DEFS \
X(glyph0, { '0' _ 0x0E _ 0x11 _ 0x0E _ 0 } ) \
X(glyph1, { '1' _ 0x09 _ 0x1F _ 0x01 _ 0 }) \
X(glyph2, { '2' _ 0x13 _ 0x15 _ 0x09 _ 0 }) \
X(glyph3, { '3' _ 0x15 _ 0x15 _ 0x0A _ 0 }) \
X(glyph4, { '4' _ 0x18 _ 0x04 _ 0x1F _ 0 }) \
X(glyph5, { '5' _ 0x1D _ 0x15 _ 0x12 _ 0 }) \
X(glyph6, { '6' _ 0x0E _ 0x15 _ 0x03 _ 0 }) \
X(glyph7, { '7' _ 0x10 _ 0x13 _ 0x0C _ 0 }) \
X(glyph8, { '8' _ 0x0A _ 0x15 _ 0x0A _ 0 }) \
X(glyph9, { '9' _ 0x08 _ 0x14 _ 0x0F _ 0 }) \
X(glyphA, { 'A' _ 0x0F _ 0x14 _ 0x0F _ 0 }) \
X(glyphB, { 'B' _ 0x1F _ 0x15 _ 0x0A _ 0 }) \
X(glyphC, { 'C' _ 0x0E _ 0x11 _ 0x11 _ 0 }) \
X(glyphD, { 'D' _ 0x1F _ 0x11 _ 0x0E _ 0 }) \
X(glyphE, { 'E' _ 0x1F _ 0x15 _ 0x15 _ 0 }) \
X(glyphF, { 'F' _ 0x1F _ 0x14 _ 0x14 _ 0 }) \
#define X(name, data) const uint8_t name [] = data ;
GLYPH_DEFS
#undef X
#define X(name, data) name _
const uint8_t *const glyphs[] = { GLYPH_DEFS nullptr };
#undef X
#undef _
So, when processing the X
macro, we pass it a pile of _
s, which aren't commas, so it doesn't complain. Then we expand the _
macro and voila: we have syntactically valid array initalizers. If Andrea ever changes the list of glyphs, adding or removing any, the macro will automatically sync the declaration of the individual arrays and their pointers over in the glyphs
array.
Andrea adds:
The scope of this definition is limited to this data structure, in which the X macros are used, and it is #undef'd just after that. However, with all the stories of #define abuse on this site, I feel I still need to atone.
The testing sketch works perfectly.
Honestly, all sins are forgiven. There isn't a true WTF here, beyond "the C preprocessor is TRWTF". It's a weird, clever hack, and it's interesting to see this technique in use.
That said, as you might note: this was a testing sketch, just to prove a concept. Instead of getting clever with macros, your disposable testing code should probably just get to proving your concept as quickly as possible. You can worry about code maintainability later. So, if there are any sins by Andrea, it's the sin of overengineering a disposable test program.