Navigating pitfalls of C/C++ macros

Macros are widely and correctly considered harmful. It’s generally a bad idea to use a macro where a function, constant, enum, or any non-macro construct can accomplish the same thing. However, this article isn’t about convincing you not to use macros. I assume the reader, like all good C and C++ programmers, already has a healthy distrust of macros. Rather, I’d like to offer some brief guidance for those occasions when a macro is justified and useful. In those cases, it pays to be aware of the specific dangers that macros pose and some ways to avoid them while still getting use out of your macro.

This article will proceed by way of examples—first a problem, then a solution. The first three types of problems I discuss can be seen as special cases of “accidental syntactic interactions at macro usage sites”. This issue is somewhat subtle, so I treat it as three separate types.

Problem: Expression in macro interacts unintentionally with macro arguments

Suppose we have the following code:

#define TIMES_TWO(x) (x * 2)

void foo() {
  printf("A: %d\n", TIMES_TWO(2));
  printf("B: %d\n", TIMES_TWO(1 + 1));
}

We expect to see A: 4 B: 4, but instead we get A: 4 B: 3. Why?

Inside the macro, x is replaced with 1 + 1, so the preprocessed code becomes 1 + 1 * 2, which is clearly 3, even though the writer of TIMES_TWO(1 + 1) probably expects it to behave the same as TIMES_TWO(2).

Solution: Wrap expressions in parentheses

If an argument to a function-like macro may be an expression (as opposed to, say, just a single token), you should surround all the macro’s usages of that argument with parentheses.

#define TIMES_TWO(x) ((x) * 2)

Replacing x inside the macro with (x) solves the problem by forcing the substituent to be parsed as a single expression rather than as just a string of tokens.

Problem: Statement in macro unintentionally parented to something at usage site

Consider the following macro.

#define COPY_AB(x, y) x.a = y.a; x.b = y.b;

This macro will do what’s expected, but only in some syntactic contexts. Suppose we have the following code.

typedef struct {
  int a;
  int b;
  /* possibly other members */
} ab_struct;

void print_ab(const ab_struct* ab) {
    printf("a = %d, b = %d\n", ab->a, ab->b);
}

void foo() {
  ab_struct p = {0};
  ab_struct q = {.a = 1, .b = 2};
  COPY_AB(p, q);
  print_ab(&p);
}

foo() prints a = 1, b = 2 as expected. But now suppose we change the function a little.

void foo(int v) {
  ab_struct p = {0};
  ab_struct q = {.a = 1, .b = 2};
  if (v > 5)
    COPY_AB(p, q);
  print_ab(&p);
}

If we call foo(0), we expect to see a = 0, b = 0, since the condition v > 5 won’t be met. But instead, we actually see a = 0, b = 2. What gives?

If we examine the preprocessed version of foo, the problem becomes clear:

void foo(int v) {
  ab_struct p = {0};
  ab_struct q = {.a = 1, .b = 2};
  if (v > 5)
    p.a = q.a; p.b = q.b;
  print_ab(&p);
}

If that’s still not clear, let’s clean up the indentation.

void foo(int v) {
  ab_struct p = {0};
  ab_struct q = {.a = 1, .b = 2};
  if (v > 5)
    p.a = q.a;
  p.b = q.b;
  print_ab(&p);
}

GCC actually warns about this situation with -Wall (or -Wmultistatement-macros). It seems Clang and MSVC lack an equivalent warning at the time of writing.

Solution

See next section’s solution.

Problem: Statement at usage site unintentionally parented to something in macro

Now suppose we had a different macro:

#define MAYBE_INCREMENT(c, x) if (c) { ++x; }

and a function that uses it:

int bar(int x, int y) {
  int i = 1;
  if (x > 0)
    MAYBE_INCREMENT(y > 0, i)
  else
    --i;
  return i;
}

At first glance, bar(1, 0) should return 1 (that is, i won’t be changed from its initial value). But instead, bar(1, 0) returns 0. Why?

If we rewrite bar by substituting the macro and using more natural whitespace, it becomes the following.

int bar(int x, int y) {
  int i = 1;
  if (x > 0)
    if (y > 0) {
      ++i;
    }
    else
      --i;
  return i;
}

The problem is that the parser considers the else in bar to be part of the macro’s if statement, not the if that appears in bar.

Solution: Wrap with do…while (0)

The main solution to problems involving macros that expand to multiple statements or to a statement (like if) that can bind to other syntax in context (like else) is to wrap your macro definition in do { ... } while (0). This has the effect of ensuring the parser takes all your macro statements together and prevents unwanted interaction with other syntactic constructs at the macro’s usage sites. It also forces users of the macro to follow it with a semicolon, which helps express its intended usage and makes it look like a normal statement.

#define COPY_AB(x, y) do { x.a = y.a; x.b = y.b; } while (0)
#define MAYBE_INCREMENT(c, x) do { if (c) { ++x; } } while (0)

Problem: Accidental reevaluation of function-like macro arguments

When you call a function, the arguments are evaluated at the call site, and it’s as if the callee receives its own copy of each argument. Function-like macros generally do not behave in this way. Instead, the inputs to a function-like macro are lexically expanded in the macro body everywhere the input is named. Unlike with functions, this can cause the macro’s arguments to be evaluated more than once.

typedef struct {
  int start;
  int end;
} error_info;

#define ERROR(index, length) \
  errorInfo.start = index; \
  errorInfo.end = index + length; \
  goto error;

void process()
  error_info errorInfo;
  /* ... */
  int i = 0;
  ERROR(i++, 2);
  /* ... */
  error: /* ... */
}

This may look like a weird example, but the point is that the macro argument index appears more than once in the macro. Each time ERROR(i++, 2) is expanded, index will be replaced with i++. Since i++ has side effects, we get markedly different behavior from what we’d get if ERROR were a function instead of a macro. In particular, if ERROR were defined similarly but as a function, errorInfo would end up holding {.start = 0, .end = 2}. With the macro written as it appears above, we instead get the surprising {.start = 0, .end = 3}.

Solution: Make a temporary inside the macro

You can try declaring a temporary variable inside the macro and using it instead of the argument directly. This reduces the number of evaluations of index to 1.

#define ERROR(index, length) \ do {
  int i = index; \
  errorInfo.start = i; \
  errorInfo.end = i + length; \
  goto error; } while (0)

Problem: Names declared in macros can collide with names in the usage site

While sometimes it’s necessary to declare a macro-scoped variable inside a macro, there’s a risk that what you declare in your macro will have the same name as something in a scope where the macro is used.

Suppose we have the following code:

#define PRINT_SUM(x, y) do { \
  int sum = (x) + (y); \
  printf("sum = %d\n", sum); \
  } while (0)

void test() {
  PRINT_SUM(1, 2);
}

This will print sum = 3 as expected. But now consider a different version of test:

void test() {
  int sum = 1;
  PRINT_SUM(sum, 2);
}

A reader of test would expect this to print 3 as before, but instead, the program now has undefined behavior. This is because x in the macro is replaced with the token sum, which refers to the sum declared in the macro, not the sum declared in test. This amounts to reading sum before it is initialized, which is UB. Additional versions of this problem can occur that don’t trigger UB but still behave in ways the programmer did not intend.

Solution: Choose a weirder name

There’s no perfect solution for this. The go-to is to pick a name in your macro that is unlikely to be used elsewhere. In the PRINT_SUM example, the macro’s sum declaration could be renamed to something like __macro_print_sum_sum1.

Conclusion

The problems presented here, even though most have solid workarounds, should serve as reasons to avoid macros when possible. When you do work with macros, I recommend following the advice herein by default, not just reactively adopting it in select places where you encounter weird errors. The advice is nasty, but the surprises it avoids are nastier. Therefore, observing it as convention will make you less likely to run into the rough edges of macros in the first place.


  1. (looking past the fact that this macro doesn’t need to declare a variable at all, and that it doesn’t need to be a macro at all) ↩︎

Posted

in

by