The C preprocessor and macros are one of the most useful elements for writing portable C/C++ code . It even can be used to completely different purposes, like text processing. However, it suffers one major drawback: it does not support nested macros expansion (put differently, macros within macros).
So for example, the following would not work as you expect:
#include <stdio.h> #define OUTER_MACRO(a,b) (a ## b) #define INNER_MACRO(a) (#a) const char *str1 = "hello world"; void main (void) { // Displays "Hello world" printf ("Output is: %s\n", OUTER_MACRO(st,r1)); // Displays "r1" printf ("%s", INNER_MACRO (r1)); // .. but this one causes compiler error printf ("Output is: %s", OUTER_MACRO (st, INNER_MACRO (r1))); }
Both OUTER_MACRO and INNER_MACRO work as expected, but not in composition. There are however situations where this is desirable.
For example, in the following code:
*fp = fopen (ACCEL_DATA_FILE, "r");
ACCEL_DATA_FILE refers to, as the name suggest, some acceleration data gathered by an instrumentation. As we are interested that the file name for these “Acceleration data” reflect also some date/time, we need to find a way to once open accel_data_20171013.dat and later accel_data_1027104.dat. However, our client code should not change.This can be achieved with a smart macro toolbox, as follows.
First, we define some utility macros (tools_macros.h)
#ifndef __BIZARRE_MACROS__ #define __BIZARRE_MACROS__ #define STRINGIFY(X) STRINGIFY2(X) #define STRINGIFY2(X) #X #define CAT(X,Y) CAT2(X,Y) #define CAT2(X,Y) X##Y #define INCLUDE_FILE(HEAD,TAIL) STRINGIFY(CAT(HEAD,TAIL) ) #endif
The whole secret here is to do a “double expansion”, so that, contrary to the small test program above, each level of expansion produces something meaningful.
Now we can write
#include "tools_macros.h" #define REL_PATH .\\data #define ACCEL_DATA_FILE STRINGIFY(CAT(REL_PATH,\\session_accel.c))
the outer macro would expand as
“CAT(REL_PATH, \\session_accel.c)”
so our line fopen would then include one single macro which expands correctly :). Following the same principle, we can also write:
const int16_t a_test_data[] = { #include INCLUDE_FILE(SESSION_PREFIX,_accel.c) };
where the inner generated file contains only data lines
-1689,1481,288, -1526,1605,168, -1564,1063,-417, -1677,621,-827, -1737,1157,-243, -1589,1194,-637, -1364,790,-584, -1420,924,-541, -1610,1386,-453, -1534,1361,-494, -1462,1249,-632, -1487,1354,-551, -1375,1429,-509, -1333,1413,-534, -1237,1359,-623, -1424,1350,-586, -1329,1548,-674, -1352,1472,-841, -1482,1601,-1215, -1405,2517,-804, -1339,3469,378, -1690,2384,-218,
Those lines are the typical output of some instrumentation (X,Y,Z accelerations). Using these tricky macros once again allowed us to reach the code / data isolation paradigm.