Macros

A function-like macro takes arguments and pastes them into its body before the compiler sees them. It has no type checking because there is no function: the arguments are text. The parenthesis and the placement of every parameter decide whether the macro behaves like a function or quietly changes the meaning of an expression.

Function-like macros

The body of MAX parenthesises each parameter and the whole expression. Without the outer pair, a caller's MAX(a, b) * 2 would multiply only one branch. With them the macro behaves like a function call for every expression that has no side effect.

#include <stdio.h> #define MAX(a, b) (((a) > (b)) ? (a) : (b)) int main(void) { printf("%d\n", MAX(3, 7)); printf("%d\n", MAX(-2, -9)); printf("%d\n", MAX(2 + 3, 4 * 1)); return 0; }

gcc -Wall -Wextra max.c -o max
./max

7
-2
5

MAX(-2, -9) is -2 because the comparison is between the two values, not between their absolute sizes. The third call shows the parentheses working: the addition and the multiplication stay inside their parameters.

Multi-statement macros

A macro that expands to several statements must be a single statement from the caller's point of view, or an if without braces will attach to the wrong part. The do { } while (0) wrapper is the standard answer: the semicolon after the call completes the loop, and the block runs exactly once.

#include <stdio.h> #define LOG(value) do { \ printf("value: %d\n", (value)); \ } while (0) int main(void) { int flag = 1; if (flag) LOG(10); else LOG(20); return 0; }

gcc -Wall -Wextra dowhile.c -o dowhile
./dowhile

value: 10

The else belongs to the if because the expansion is one statement. A plain { ... } wrapper would compile but leaves a semicolon after the block, which is a trap in an if/else chain.

Stringify and paste

Two operators work only inside macros. A single # before a parameter turns its text into a string literal, and ## joins two tokens into one. Both happen before compilation, so the result can be a name the source never spells out.

#include <stdio.h> #define STRINGIFY(x) #x #define CONCAT(a, b) a##b int main(void) { int CONCAT(total, 1) = 42; printf("%s\n", STRINGIFY(hello)); printf("%s\n", STRINGIFY(2 + 3)); printf("total1: %d\n", total1); return 0; }

gcc -Wall -Wextra stringify.c -o stringify
./stringify

hello
2 + 3
total1: 42

The stringification does not evaluate the argument: it keeps the spaces and the signs exactly as written, which is why STRINGIFY(2 + 3) prints the expression. Token pasting happens before the name is looked up, so total1 exists as a normal variable in the line after the macro.

Variadic macros

... in the parameter list collects the remaining arguments in __VA_ARGS__. When a call passes none, the comma before the group would remain, so C23 added __VA_OPT__(,), which expands to a comma only when arguments are present.

#include <stdio.h> #define LOGF(format, ...) printf("[log] " format "\n" __VA_OPT__(,) __VA_ARGS__) int main(void) { LOGF("started"); LOGF("value %d at %s", 7, "noon"); return 0; }

gcc -Wall -Wextra variadic.c -o variadic
./variadic

[log] started
[log] value 7 at noon

The format is concatenated with the prefix at compile time, so the compiler still checks it: a wrong conversion or argument type in LOGF produces the same warning as in printf. That is the advantage over a hand-written variadic function, which the next lesson covers.

Double evaluation and missing parentheses

A macro argument is pasted as text, so an argument with a side effect runs once per use. MAX(i++, 0) increments i twice, and the result depends on the comparison branch:

#include <stdio.h> #define MAX(a, b) (((a) > (b)) ? (a) : (b)) #define DOUBLE(x) x * 2 int main(void) { int i = 1; int big = MAX(i++, 0); printf("big: %d, i: %d\n", big, i); printf("double: %d\n", DOUBLE(1 + 2)); return 0; }

gcc -Wall -Wextra sideeffect.c -o sideeffect
./sideeffect

big: 2, i: 3
double: 5

The first line shows two increments, the second shows the missing parentheses: DOUBLE(1 + 2) becomes 1 + 2 * 2, which is 5. A function does neither, which is why macros are for constants, logging and generic code, not for arithmetic with side effects.

Exercises

  1. Write a MIN3 macro for three values and test it with negative numbers and expressions.
  2. Write a CHECK(condition) macro that prints the file and line when the condition is false, using __FILE__ and __LINE__.
  3. Add a prefix parameter to LOGF so the same macro can log with two different tags, then call it with and without extra arguments.

Questions

A new set of questions in a new order every time. Choose one answer for each, then check.

  1. Why does MAX parenthesise every parameter and the whole body?

    Answer

    So a surrounding operator cannot split the expression

    Without the parentheses, MAX(a, b) * 2 would attach the multiplication to one branch of the conditional.

  2. Why does a multi-statement macro use do { } while (0)?

    Answer

    It makes the expansion a single statement

    The wrapper lets the trailing semicolon complete one statement, so an if without braces cannot capture the wrong part.

  3. What does the # operator do inside a macro?

    Answer

    It turns a parameter into a string literal

    Stringification keeps the argument text exactly as written, including spaces and operators.

  4. What does __VA_OPT__(,) provide in a variadic macro?

    Answer

    A comma only when extra arguments exist

    C23 uses __VA_OPT__ to avoid a stray comma when the call passes no variadic arguments.

  5. What does this program print? #include <stdio.h> #define MAX(a,b) (((a)>(b))?(a):(b)) int main(void){ printf("%d %d %d\n", MAX(3,7), MAX(-2,-9), MAX(2+3,4*1)); return 0; }

    Answer

    7 -2 5

    The macro compares the actual values, so -2 beats -9, and the parentheses keep the expressions grouped.

  6. What does this program print? #include <stdio.h> #define LOG(v) do { printf("value: %d\n", (v)); } while (0) int main(void){ int flag=1; if(flag) LOG(10); else LOG(20); return 0; }

    Answer

    value: 10

    The do-while expansion is one statement, so the else stays with the if and only the true branch runs.

  7. What does STRINGIFY(2 + 3) produce?

    Answer

    The text 2 + 3, not the value 5

    Stringification does not evaluate the argument; it keeps the tokens and spaces exactly as written.

  8. What does this program print? #include <stdio.h> #define MAX(a,b) (((a)>(b))?(a):(b)) #define DOUBLE(x) x*2 int main(void){ int i=1; int big=MAX(i++,0); printf("big: %d, i: %d\n", big, i); printf("double: %d\n", DOUBLE(1+2)); return 0; }

    Answer

    big: 2, i: 3 / double: 5

    MAX(i++,0) evaluates i++ twice, and DOUBLE(1+2) expands to 1 + 2*2, which is 5.

  9. What is the danger of MAX(i++, 0)?

    Answer

    The argument with a side effect runs twice

    A macro pastes its argument as text, so an increment appears in both branches of the conditional and runs twice.

  10. What goes wrong when a multi-statement macro is a plain block in an if?

    Answer

    The semicolon ends the if and the else breaks

    A { ... } expansion followed by a semicolon leaves the else unattached, which is why the do-while wrapper exists.

The code on this page was run with gcc (Alpine 15.2.0) 15.2.0 on Alpine Linux 3.24.3 (x86_64) in a Docker container.

Article author: Arthur Isaev