Author: R. Koucha
Last update: 4-Feb-2023
Best practices for the C language preprocessor

In C language, the preprocessing is the step prior to the source code compilation. It is a powerful mechanism which provides, among other things, the conditional compilation, the file inclusion and the expansion of macro-instructions. Those facilities are simple at first sight but they are error prone. If they are not used with care, one may face compilation errors or tricky program malfunctions. Moreover, the GCC preprocessor provides additional features which appear to be very useful.

Foreword
1. A little known failure of the compiler
2. Macro ending
3. Group instructions
4. Put parentheses around the parameters
5. Put parenthesis around the expressions
6. Avoid passing expressions as parameters
7. Variable number of parameters
8. Avoid defining variables in macros
9. Special macros
    9.1. __GNUC__
    9.2. __LINE__, __FILE__ et __FUNCTION__
    9.3. "#" operator
    9.4. "##" operator
10. How to get rid of GCC attributes
11. Conditional inclusions
12. X macros
13. Conclusion
Links
References
About the author
Foreword

A french version of this article has been published in  glmf_logo number 105.

1. A little known failure of the compiler

We can define and use constants as follow:

1  #include <stdio.h>
2
3  #define CONSTANT_1 (0x3d+20)
4  #define CONSTANT_2 (0x3e+20)
5  #define CONSTANT_3 (0x3f+20)
6
7  int main(int ac, char *av[])
8  {
9    printf("%u\n", CONSTANT_1);
10   printf("%u\n", CONSTANT_2);
11   printf("%u\n", CONSTANT_3);
12 }

When compiling this program, we get an unexpected error:

$ gcc try_1.c
try_1.c: In function 'main':
try_1.c:4:21: error: invalid suffix "+20" on integer constant
#define CONSTANT_2 (0x3e+20)
                    ^
try_1.c:10:19: note: in expansion of macro 'CONSTANT_2'
printf("%u\n", CONSTANT_2);

The compiler detected an error at line 4, column 21 that is to say in the CONSTANT_2 macro. This is a weakness of the compiler which believes to see an integer (0x3) followed by an exponent (e+20). In other words it sees 0x3 multiplied by 10 power 20. This is not a legal notation in C language as exponents are not allowed for hexadecimal contants like "0x3". To fix this issue, we need to rewrite CONSTANT_2 with white spaces between "e" and "+":

#define CONSTANT_2 (0x3e + 20)

This makes the compilation and execution work:

$ gcc try_2.c
$ ./a.out
81
82
83

Thus, it is advised to systematically put blank chars around "+" and "-" operators and when we define macros with the "e" hexadecimal number.

2. Macro ending

It is not advised to finish a macro with ";" as shown in the following example:

1  #define ADD(a, b) a + b;
2
3  int main(int ac, char *av[])
4  {
5  int r = ac;
6
7    if (r)
8      r = ADD(4, 5);
9    else
10     r = ADD(5, 6);
11
12   return 0;
13 }

The preceding triggers a compilation error:

$ gcc try_3.c
try_3.c: In function 'main':
try_3.c:9:3: error: 'else' without a previous 'if'
   else
   ^~~~

The line number 9 contains two instructions because of the ";" coming from the ADD macro. The preprocessed file points this out:

$ gcc -E try_3.c
[...]
if (r)
  r = 4 + 5;;
else
  r = 5 + 6;;

The else instruction is orphan as the if to which it is supposed to be linked contains more than one instruction which are not surrounded by braces: "r = 4 + 5;" and the empty ";" instruction.

3. Group instructions

When a macro embeds several instructions as shown in this example:

#define ADD(a, b, c) \
           printf("%d + %d ", a, b); \
           c = a + b; \
           printf("= %d\n", c);

We may face the problem presented in the preceding paragraph if it is used in a if statement as the user of the macro may not put braces around the macro call. Or perhaps the macro originally did not contain multiple instructions and a software modification required to add more instructions into it. This is dangerous to write macros in this way because it introduces tricky bugs. For example, if we use it without braces in a while statement:

#include <stdio.h>

#define ADD(a, b, c) \
           printf("%d + %d ", a, b); \
           c = a + b; \
           printf("= %d\n", c);

int main(int ac, char *av[])
{
int i = 0;
int r = 0;

  while (av[i++])
    ADD(r, 1, r);

  return 0;
}

We expect that ALL the instructions composing ADD() be executed upon each loop iteration instead of:

$ gcc try_4.c
$ a.out 1 1 1
0 + 1 0 + 1 0 + 1 0 + 1 = 1

Actually, the preprocessor will generate a set of instructions without surrounding braces and consequently only the first instruction (i.e. printf()) will be executed upon each loop iteration:

$ gcc -E try_4.c
[...]
int main(int ac, char *av[])
{
int i = 0;
int r = 0;

  while (av[i++])
    printf("%d + %d ", r, 1); r = r + 1; printf("= %d\n", r);;

  return 0;
}

To prevent this kind of tricky error, it is advised to group the instructions of the macro in a "do-while" not followed by a ";" (according to the rule of § 2). This makes the multi-instructions macro appear as one (meta-)instruction:

#define ADD(a, b, c) do { \
                       printf("%d + %d ", a, b); \
                       c = a + b; \
                       printf("= %d\n", c); \
                     } while(0)

The compiler will not generate a loop as the iteration condition is always false: while(0). Do not merely put braces without the beginning "do" and terminating "while(0)" as we may face the problem presented in § 2 if this macro is used in a "if-else" with a terminating ";". Writing the macro without terminating ";", the user is obliged to put a ";" after calling the macro otherwise the compiler ends in error. The macro will be considered as a single (meta-)instruction: the "do-while(0)".

$ gcc -E try_5.c
[...]
int main(int ac, char *av[])
{
int i = 0;
int r = 0;

  while (av[i++])
    do { printf("%d + %d ", r, 1); r = r + 1; printf("= %d\n", r); } while(0);

  return 0;
}

Hence the expected result:

$ gcc try_5.c
$ a.out 1 1 1
0 + 1 = 1
1 + 1 = 2
2 + 1 = 3
3 + 1 = 4
4. Put parentheses around the parameters

Let's consider the following small program which defines and uses a macro executing an integer division:

#include <stdio.h>

#define DIV(a, b, c) do { \
                       printf("%d / %d ", a, b); \
                       c = a / b; \
                       printf("= %d\n", c); \
                     } while(0)

int main(int ac, char *av[])
{
int r = 5;
int v1 = 12;
int v2 = 6;

  DIV(v1, v2, r);
  DIV(v1 + 6, v2, r);

  return 0;
}

At execution time we expect the display of the value "2" for the first division (i.e. 12 / 6) and 3 for the second ((12 + 6) / 6). But we get the following:

$ ./a.out
12 / 6 = 2
18 / 6 = 13

As usual, when we have some doubts when manipulating macros, we look at the output of the preprocessor:

int main(int ac, char *av[])
{
int r = 5;
int v1 = 12;
int v2 = 6;

  do { printf("%d / %d ", v1, v2);
       r = v1 / v2;
       printf("= %d\n", r);
     } while(0);
  do { printf("%d / %d ", v1 + 6, v2);
       r = v1 + 6 / v2;
       printf("= %d\n", r);
     } while(0);

  return 0;
}

We can see that we face an operator precedence problem in the second call to DIV(). In "r = v1 + 6 / v2" expression, the "/" operator has higher precedence than the "+" operator. So, the compiler generates the equivallent of this mathematical operation: r = v1 + ( 6 / v2). Hence, the display of the value "13" as result of the second DIV() call when "v1" and "v2" are respectively equal to "12" and "6". To solve this, we must systematically surround the parameters with parentheses each time they appear in the macro:

#define DIV(a, b, c) do { \
                         printf("%d / %d ", (a), (b)); \
                         (c) = (a) / (b); \
                         printf("= %d\n", (c)); \
                        } while(0)

Now, DIV() is correct in the output of the preprocessor:

do { printf("%d / %d ", (v1 + 6), (v2));
     (r) = (v1 + 6) / (v2);
     printf("= %d\n", (r));
   } while(0);
5. Put parenthesis around the expressions

Let's consider the constant defined with the CONSTANT macro and use as follow:

#include <stdio.h>

#define BASE 2000
#define CONSTANT BASE + 2

int main(int ac, char *av[])
{
  printf("%d\n", CONSTANT / 2);

  return 0;
}

The expected display is the result of "2002 / 2" operation. That is to say "1001". But we get "2001" instead.

$ gcc try_6.c
$ ./a.out
2001

The preprocessor output shows that there is an operator precedence problem as seen previously

printf("%d\n", 2000 + 2 / 2);

So, when a macro is an expression (constant, test, ternary operator...), it is advised to surround it with parenthesis. Hence the following fix in our example:

#define BASE (2000)
#define CONSTANT (BASE + 2)
6. Avoid passing expressions as parameters

We often use macros as if they were functions or some code changes in an existing software may trigger the replacement of functions by macros (readability, code inlining for the sake of optimization...). Here is an example which defines and uses the IS_BLANK(c) macro which returns true if the passed parameter is a blank character (i.e. space or tabulation):

#include <stdio.h>

#define IS_BLANK(c) ((c) == '\t' || (c) == ' ')

int main(int ac, char *av[])
{
char *p = av[0];

  while(*p)
  {
    if (IS_BLANK(*(p++)))
    {
      printf("Blank at index %d\n", (int)((p - 1) - av[0]));
    }
  }

  return 0;
}

This program is supposed to display the indexes of the blank characters in its name. But the displayed indexes are incorrect and some of the blanks are not seen:

$ gcc main.c -o "name with spaces"
$ ./name\ with\ spaces
Blank at index 11

The root cause of the problem is the fact that "c" parameter is interpreted twice in the IS_BLANK() macro. The first time to be compared to "\t" and the second to be compared to " ". As "*(p++)" is passed, it means that "p" is incremented after each comparisons as shown by the output of the preprocessor:

if (((*(p++)) == '\t' || (*(p++)) == ' '))

In other words, the first comparison is done with the character pointed by "p". The latter being post-incremented, points on the following character when the second expression is evaluated. Then, at the end of the macro, "p" is one more time incremented. So, we must avoid to pass expressions as parameters to the macros.

#include <stdio.h>

#define IS_BLANK(c) ((c) == '\t' || (c) == ' ')

int main(int ac, char *av[])
{
char *p = av[0];

  while(*p)
  {
    if (IS_BLANK(*p))
    {
      printf("Blank at index %d\n", (int)(p - av[0]));
    }

    p ++;
  }

  return 0;
}

It is not easy to follow this rule. Moreover, if IS_BLANK was a function and became a macro after code changes, a complete code review is necessary. But if the macro is in a header file used all over the world, this may become "Mission: impossible"!

GCC provides an specific extension, so may be not an ANSI C standard one, which helps convert a block of instructions (i.e. set of instructions surrounded by braces) into an expression (cf. [1]). As it is also possible to define local variables in blocks, the IS_BLANK() macro can be rewritten in a robust way:

#include <stdio.h>

#define IS_BLANK(c) ({char _c = c; ((_c) == '\t' || (_c) == ' ');})

int main(int ac, char *av[])
{
char *p = av[0];

  while(*p)
  {
    if (IS_BLANK(*(p++)))
    {
      printf("Blank at index %d\n", (int)((p - 1) - av[0]));
    }
  }

  return 0;
}

The "_c" local variable stores the value of the parameter. Then, we use this variable instead of the parameter in the rest of the macro. This makes the parameter value interpreted once (when "_c" is assigned) as expected.

$ gcc main.c -o "name with spaces"
$ ./name\ with\ spaces
Blank at index 6
Blank at index 11
7. Variable number of parameters

The ISO C99 standard provides the ability to define macros with a variable number of arguments. This can be done in two ways:

1  #include <stdio.h>
2
3  #define DEBUG(fmt, ...) \
4           fprintf(stderr, "Line %d: " fmt "\n", __LINE__, __VA_ARGS__)
5
6  #define DEBUG2(fmt, args...) \
7           fprintf(stderr, "Line %d: " fmt "\n", __LINE__, args)
8
9  int main(int ac, char *av[])
10 {
11    DEBUG("Program's name = %s", av[0]);
12    DEBUG("Message without parameters");
13
14    DEBUG2("Program's name = %s", av[0]);
15    DEBUG2("Message without parameters");
16
17    return 0;
18 }

DEBUG() and DEBUG2() are overloads of fprintf() to display formatted debug messages. The "fmt" parameter concatenated with strings is the format passed as first parameter of the displaying function. The __VA_ARGS__ and args variants represent the variable number of comma separated parameters. The latter notation is generally preferred as it permits to name the arguments (for example, here we use args instead of __VA_ARGS__). This variant was specific to GCC before the feature became a standard. In old GCC versions, this is the only accepted notation. Even if they are convenient, those notations have a major limitation: they don't allow the calls without parameters in the variable part. Here is the result of the compilation followed by the preprocessor output:

$ gcc vaargs.c 
vaargs.c: In function 'main':
vaargs.c:4:74: error: expected expression before ')' token
    4 |               fprintf(stderr, "Line %d: " fmt "\n", __LINE__, __VA_ARGS__)
      |                                                                          ^
vaargs.c:12:4: note: in expansion of macro 'DEBUG'
   12 |    DEBUG("Message without parameters");
      |    ^~~~~
vaargs.c:7:67: error: expected expression before ')' token
    7 |               fprintf(stderr, "Line %d: " fmt "\n", __LINE__, args)
      |                                                                   ^
vaargs.c:15:4: note: in expansion of macro 'DEBUG2'
   15 |    DEBUG2("Message without parameters");
      |    ^~~~~~
$ gcc -E vaargs.c
[...]
# 9 "vaargs.c"
 int main(int ac, char *av[])
 {
   fprintf(
# 11 "vaargs.c" 3 4
  stderr
# 11 "vaargs.c"
  , "Line %d: " "Program's name = %s" "\n", 11, av[0]);
   fprintf(
# 12 "vaargs.c" 3 4
  stderr
# 12 "vaargs.c"
  , "Line %d: " "Message without parameters" "\n", 12, );

   fprintf(
# 14 "vaargs.c" 3 4
  stderr
# 14 "vaargs.c"
  , "Line %d: " "Program's name = %s" "\n", 14, av[0]);
   fprintf(
# 15 "vaargs.c" 3 4
  stderr
# 15 "vaargs.c"
  , "Line %d: " "Message without parameters" "\n", 15, );

   return 0;
 }

The errors come from the comma located before the empty list of parameters at the second and fourth call to fprintf(). GCC provides a useful extension through "##" notation to suppress the comma when the following parameter list is empty. Hence the new version of the macros:

1  #include <stdio.h>
2
3  #define DEBUG(fmt, ...) \
4          fprintf(stderr, "Line %d: " fmt "\n", __LINE__, ##__VA_ARGS__)
5
6  #define DEBUG2(fmt, args...) \
7          fprintf(stderr, "Line %d: " fmt "\n", __LINE__, ##args)
8
9  int main(int ac, char *av[])
10 {
11   DEBUG("Program's name = %s", av[0]);
12   DEBUG("Message without parameters");
13
14   DEBUG2("Program's name = %s", av[0]);
15   DEBUG2("Message without parameters");
16
17   return 0;
18 }
$ gcc vaargs.c
$ ./a.out 
Line 11: Program's name = ./a.out
Line 12: Message without parameters
Line 14: Program's name = ./a.out
Line 15: Message without parameters
8. Avoid defining variables in macros

Generally, it is not advised to define variables in macros. This rule is controversial as the C language allows to define variables at different scope level. But let's consider a macro which defines a local variable in a block as described in § 3:

#define ERR() do { \
                  int err = errno; \
                  logging_function(errno); \
                  errno = err; \
              } while (0)

As the variable is defined in a block, it will not conflict with variables of the same name in outer scopes. So, the following code compiles and works correctly:

#include <errno.h>

#define ERR() do { \
                  int err = errno; \
                  logging_function(errno); \
                  errno = err; \
              } while (0)


void logging_function(int err_code)
{
  // Do some stuff...
}


int syscall(void)
{
  // Do some stuff...
  return -1;
}


int func(void)
{
int err;

  err = syscall();
  if (err != 0)
    ERR();

}

int main(void)
{
  func();
}

But, some design environments enforce controls on variable naming to make sure that some part of a source code do not use a variable in a given scope believing that it is in an outer scope. To make it, they activate both -Wshadow and -Werror flags on the command line of the gcc compiler. In this context, the preceding code would not compile:

$ gcc -Wshadow -Werror try10.c
try10.c: In function 'func':
try10.c:4:23: error: declaration of 'err' shadows a previous local [-Werror=shadow]
    4 |                   int err = errno; \
      |                       ^~~
try10.c:29:5: note: in expansion of macro 'ERR'
   29 |     ERR();
      |     ^~~
try10.c:25:5: note: shadowed declaration is here
   25 | int err;
      |     ^~~
cc1: all warnings being treated as errors
So, avoid as much as possible to define variables in macros. Especially, when those macros are part of a header file of an API which can be included by various projects all over the world. Using names which are likely not going to appear in sources codes would workaround this:
#define ERR() do { \
                  int __err__ = errno; \
                  logging_function(errno); \
                  errno = err; \
              } while (0)
But it may still fail if multiple macros coming from various API are used at the same time with the same kind of tricks to name their "hidden" variables.
9. Special macros

The C preprocessor provides numerous macros and notations with specific behaviours. In this paragraph, we present some of them which are frequently used.

9.1. __GNUC__

The __GNUC__ macro is always defined when the GCC toolchain is used. So, it is advised to use it when we plan to make a project subject to be built with various toolchains (e.g. clang): this permits to isolate GCC specific extensions. Cf. § 10 for a usage example.

9.2. __LINE__, __FILE__ and __FUNCTION__

The __LINE__, __FILE__ and __FUNCTION__ macros are respectively replaced by the current source's line number, the current source's filename and the current function's name. Those facilities are typically used to make error/debug messages more accurate by pointing out a location in the sources. For example, here is a program displaying its parameters thanks to the DEBUG() macro which value added is to display the filename, function name and line number from which it is invoked:

#include <stdio.h>

#define DEBUG(fmt, args...) \
   fprintf(stderr, "%s(%s)#%d : " fmt , \
           __FILE__, __FUNCTION__, __LINE__, ## args)

int main(int ac, char *av[]) {

  int i;

  for (i = 0; i < ac; i ++) {
    DEBUG("Param %d is : %s\n", i, av[i]);
  }

  return 0;
}
$ gcc debug.c
$ ./a.out param1 param2
debug.c(main)#14: Param 0 is : ./a.out
debug.c(main)#14: Param 1 is : param1
debug.c(main)#14: Param 2 is : param2

__FUNCTION__ is GCC specific. The C standard came later with the __func__ notation. Although they display the same thing, internally they behave differently. The first expands as a constant string of characters whereas the other triggers the definition of a pointer named "__func__", local to the function and referencing the function's name as a character string. So, on one side we have a constant which can be concatenated at compilation time to other strings whereas on the other side we have a variable. But since the 3.4 release of GCC, both notations behaves as a local pointer. So, they are not expanded at preprocessing time, but at compilation time.

Here is the output of the preprocessor for the preceding program. We can see that __LINE__ and __FILE__ are respectively expanded with "20" and "try_8.c" whereas __FUNCTION__ is not as it will be managed as a symbol by the compiler:

int main(int ac, char *av[]) {
  int i;

  for (i = 0; i < ac; i ++) {
    fprintf(
# 20 "try_8.c" 3 4 stderr
# 20 "try_8.c"  , "%s(%s)#%d : " "Param %d is : %s\n" , "try_8.c", __FUNCTION__, 20, i, av[i]);
  }
  return 0;
}
9.3. "#" operator

The "#" notation converts the value of a macro parameter into a character string. For example, here is a function which displays a signal number thanks to the CASE_SIG() macro which takes advantage of the "#" notation:

#include <stdio.h>
#include <signal.h>
#include <stdlib.h>

#define CASE_SIG(s) case (s) : printf("%s\n", "SIGNAL_" #s); break

void signum(int sig) {

  switch(sig) {

    CASE_SIG(SIGINT);
    CASE_SIG(SIGTERM);
    CASE_SIG(SIGKILL);
    CASE_SIG(SIGTRAP);
    CASE_SIG(SIGSEGV);
    CASE_SIG(SIGCHLD);
    // [...]
    default: printf("???\n");
  }
}

int main(int ac, char *av[])
{
  if (ac > 1) {
    signum(atoi(av[1]));
  }
}
$ gcc signal.c
$ ./a.out 5
SIGNAL_SIGTRAP
9.4. "##" operator

We have already seen one usage of "##" in § 7. But it is possible to use it another way where this notation concatenates the the lexical elements surrounding it to make a new lexical unit. In the following example, the words CONSTA and NT are contatenated to make the CONSTANT lexical unit which is the macro name defining "233":

#include <stdio.h>

#define CONSTANT 233

#define CONCAT(a, b) a##b

int main(void) {
  printf("%d\n", CONCAT(CONSTA, NT));
}
$ gcc concat.c
$ ./a.out
233
10. How to get rid of GCC attributes

The attributes are GCC specific extensions to increase the controls and contribute to the optimization of the generated code. Let's have a look at the following example where the "format" attribute is used (cf. [2]) to get more information on the GCC attributes). funct() calls two functions making formatted display in the same way as printf(): the first parameter describes the format and the variable list of following parameters are displayed according to the requested format. The call to these functions contain a common programming error: the format requires two parameters (a string for "%s" and an integer for "%d") but only one parameter is passed:

1  extern void my_printf1 (const char *fmt, ...);
2
3  extern void my_printf2 (const char *fmt, ...)
4         __attribute__ ((format (printf, 1, 2)));
5
6
7  void funct(void) {
8
9    my_printf1("Display of %s followed by %d\n", 46);
10
11   my_printf2("Display of %s followed by %d\n", 46);
12
13 }

The compilation of this program with the "-Wall" option points out the programming error with a warning message on line 11 but not for line 9. This is because in line 11, we use the my_printf2() function defined with the format attribute to specify that it is a "printf-like" function which uses a format as parameter number 1 and a list of associated parameters beginning at parameter 2

$ gcc -c -Wall try_9.c
try_9.c: In function 'funct':
try_9.c:11:27: warning: format '%s' expects argument of type 'char *', but argument 2 has type 'int' [-Wformat=]
   my_printf2("Display of %s followed by %d\n", 46);
try_9.c:11:42: warning: format '%d' expects a matching 'int' argument [-Wformat=]
   my_printf2("Display of %s followed by %d\n", 46);

The attributes are powerful and very useful but are GCC specific. As __attribute__ is defined with one parameter (hence the double parenthesis when we passe multiple parameters to make them appear as one parameter), it is possible to use the conditional compilation to redefine __attribute__ to nothing when the compiler is not GCC.

#ifndef __GNUC__
#define __attribute__(p) // Nothing
#endif // __GNUC__

In the latter example, the conditional compilation uses __GNUC__ which is defined only if the running toolchain is GCC (cf. § 9.1).

11. Conditional inclusions

A header file is included in a source file thanks to the "#include" directive. Most of the time, those files contain external declarations of variables and functions, types and macro definitions. A header file may also include other header files as a common rule in C programming is to always make a header file independant. In other words, if a header file uses a type, a macro, a function or a variable, it is recommended that the corresponding header be included. In the example depicted in figure 1, the "main.c" file includes the str.h and fct.h header files which respectively define/declare the str_t type and the func() function. The latter two files include integer.h because both of them use INTEGER type.

figure_1

Figure 1: Multiple inclusions

The compilation of main.c triggers the following errors:

$ gcc -c main.c
In file included from fct.h:1,
from main.c:2:
integer.h:1: error: redefinition of typedef "INTEGER"
integer.h:1: error: previous declaration of "INTEGER" was here

The compiler points out that INTEGER type is defined twice. The first definition comes from str.h and the second from fct.h. Both include integer.h. As a consequence, the main.c file implicitely includes the file integer.h twice. To solve this, we use the conditional compilation to trigger the inclusion of a header file only if it is not already included. The trick consists to define a macro name specific to each header file (typically a macro name derived from the header file's name) for the sake of unicity. To illustrate this trick, here is how integer.h is modified to include it only when INTEGER_H is not defined:

#ifndef INTEGER_H
#define INTEGER_H

typedef int INTEGER;

#endif // INTEGER_H

This permits to compile main.c because INTEGER_H is defined when integer.h is first included in str.h and this will prevent the second inclusion from fct.h. As a general rule, it is advised to use this trick systematically in all header files. Figure 2 is a fixed version of figure 1 with the application of this rules.

figure_2

Figure 2: Conditional inclusions

12. X macros

The concept of X macros consists in providing multiple expansions of a macro named X. This is a robust way to keep the coherency between items and a corresponding list of services.

As an example, let's consider the following X macro which enumerates error messages and associated error labels:

// The X-macro
#define ERROR_LIST                            \
       X(ERROR_LABEL_0, "Error message#0\n")  \
       X(ERROR_LABEL_1, "Error message#1\n")  \
       X(ERROR_LABEL_2, "Error message#2\n")  \
       X(ERROR_LABEL_3, "Error message#3\n")

From there, it is possible to define X to generate enumerated integer labels:

// 1st definition of X to define enum error codes
#define X(e, m) e,
enum {
ERROR_LIST
};
#undef X

A second definition of X defines cases for a switch statement used in a function to print the error message corresponding to a given label:

// 2nd definition of X to define cases in a switch
void print_error(int e)
{
#define X(e, m) case e: fprintf(stderr, "[ERROR] : code (%d)\n\t%s", e, m); break;
  switch(e) {
    ERROR_LIST
    default: printf("[ERROR] : unknown code (%d)\n", e);
  }
#undef X
}

We can use the previous in a program like this:

int main(void)
{
  print_error(ERROR_LABEL_2);
  print_error(ERROR_LABEL_1);
  print_error(ERROR_LABEL_3);
  print_error(10);

  return 0;
}

Example of execution:

[ERROR] : code (2)
	Error message#2
[ERROR] : code (1)
	Error message#1
[ERROR] : code (3)
	Error message#3
[ERROR] : unknown code (10)

Hence, it is possible to add or suppress an error message in the X macro without doing any update in the related services as they will implicitly take in account the modifications at preprocessing time.

13. Conclusion

This article presented several rules and tips to benefit from some useful features of the C language preprocessor for the sake of robustness, portability, optimization and debuggability of the programs. We only focused on a subset of the powerful available facilities. The reader can have a look at the following links and references to go farther.

Links

[1] GCC extensions : Statements and Declarations in Expressions
[2] GCC attributes : http://www.unixwiz.net/techtips/gnu-c-attributes.html
[3] GCC Manuals : http://gcc.gnu.org/onlinedocs/

References

[4] Boulay (Nicolas), « Le C n'est pas portable », GLMF 102, Février 2008
[5] Boulay (Nicolas), « Le processus de compilation C », GLMF 103, Mars 2008
[6] Kernighan, Brian.W. &Ritchie, Dennis.M., « The C language », 2nd edition, Masson, 1990

About the author

The author is an engineer in computer sciences located in France. He can be contacted here.