C / PREPROCESSOR
When a macro should have been a function
Decide between a macro and a function on concrete grounds: name capture, argument conversion, addressability, and the jobs only macros can do.
What you will learn
- Default to static inline and let the optimizer inline small functions for you.
- Spot the capture bug where a macro's temporary hides a caller's variable.
- Explain why a prototype converts arguments while a macro cannot.
- Name the jobs only macros do: types, array sizeof, token pasting, #if constants.
Understanding When a macro should have been a function
The preprocessor does not know C. It finds an identifier, replaces it with a token sequence, and hands the result to the compiler, which then sees that token sequence sitting in the middle of whichever function wrote the macro's name. A function is the opposite kind of thing: a declared object with parameter types, a body in its own scope, and an address at run time. Nearly every macro-or-function argument settles once you ask whether the job needs tokens spliced into the caller, or a typed entity that exists on its own.
Because the body lands in the caller's scope, every name inside a macro is resolved against the caller's declarations. A macro that needs a temporary has to invent a name for it, and on the day some caller happens to use that name, the macro reads and writes the caller's variable rather than its own; nothing illegal has happened, so the code compiles and quietly computes the wrong thing. A function cannot collide this way, because its parameters and locals live in a scope the call site cannot see. Its prototype also does real work: each argument is converted to the declared parameter type and arguments that cannot convert are diagnosed, neither of which a macro can do, because it has no parameter types at all.
The historical motive for macros, avoiding the cost of a call, has largely evaporated: mark the function static inline in a header and the compiler may splice the body in, and at -O2 it usually does that for small static functions whether or not you ask. What genuinely remains macro-only is work that depends on information the compiler has already discarded by the time a call happens: taking a type instead of a value, reading sizeof on the caller's array before it decays to a pointer, pasting or stringizing tokens, recording where the call was written, and producing constant expressions for #if, case labels, and array bounds. Everything else is cheaper as a function, because you get warnings that point at your own line, a breakpoint you can set, and a name in the stack trace.
<stdio.h>
SWAP(x, y)
static void swap_int(int *x, int *y)
{
int t = *x;
*x = *y;
*y = t;
}
int main(void)
{
int a = 1, tmp = 2;
SWAP(a, tmp);
printf("macro: a=%d tmp=%d\n", a, tmp);
a = 1;
tmp = 2;
swap_int(&a, &tmp);
printf("function: a=%d tmp=%d\n", a, tmp);
return 0;
}
A macro is text spliced into the caller's scope with no parameter types and no address, so write the function unless you specifically need the caller's tokens or their compile-time type.
Worked examples
The prototype converts, the macro does not
A function's declared parameter type converts the argument at the call; a macro leaves the argument's own type in place.
<stdio.h>
HALF_MACRO(x)
static double half_func(double x)
{
return x / 2;
}
int main(void)
{
int n = 7;
printf("macro: %g\n", (double)HALF_MACRO(n));
printf("function: %g\n", half_func(n));
return 0;
}
Example explained
Line 1HALF_MACRO(n) becomes ((n) / 2), so both operands are int and the division truncates to 3.
Line 2The (double) cast only widens the 3 that was already computed; the fraction was lost before the cast ran.
Line 3half_func(n) converts n to 7.0 at the call because the prototype says the parameter is double, so the division yields 3.5.
Line 4A macro has no parameter types, so it can neither request that conversion nor reject an argument that does not fit.
A macro has no address
Callbacks need a value that exists at run time, which a text substitution never produces.
<stdio.h>
SQUARE(x)
static int square(int x)
{
return x * x;
}
static void map(int *v, int n, int (*f)(int))
{
for (int i = 0; i < n; i++)
v[i] = f(v[i]);
}
int main(void)
{
int v[4] = {1, 2, 3, 4};
map(v, 4, square);
for (int i = 0; i < 4; i++)
printf("%d%c", v[i], i == 3 ? '\n' : ' ');
printf("%d\n", SQUARE(5));
return 0;
}
Example explained
Line 1map(v, 4, square) passes the function designator, which converts to int (*)(int): a real value the loop can call.
Line 2Writing map(v, 4, SQUARE) does not compile, because SQUARE is not followed by ( and so is never expanded; the compiler then sees an undeclared identifier.
Line 3SQUARE(5) still works, which is the point: a macro exists only at the places where it is written out in full.
Line 4Anything that has to be stored or handed to other code at run time (comparators, dispatch tables, signal handlers) must be a function.
The case where the macro is right
An array's length is compile-time type information that a function can never receive.
<stdio.h>
<stddef.h>
ARRAY_LEN(a)
static size_t len_func(int a[])
{
return sizeof a / sizeof a[0];
}
int main(void)
{
int v[5] = {1, 2, 3, 4, 5};
printf("macro gives 5: %d\n", ARRAY_LEN(v) == 5);
printf("function gives 5: %d\n", len_func(v) == 5);
return 0;
}
Example explained
Line 1ARRAY_LEN(v) expands at a place where v is still an array, so sizeof (v) is the whole array and the division gives 5.
Line 2In len_func the parameter written as int a[] is adjusted to int *, so sizeof a is the pointer size and the answer is wrong.
Line 3gcc and clang warn about that line (-Wsizeof-array-argument), which is a good sign the array-shaped parameter was a lie.
Line 4The macro wins because it needs the argument's compile-time type, not its value; that is the test to apply before keeping any macro.
Important notes
-Wshadow catches the swap bug because the macro's tmp hides the caller's; it is one of the few macro hazards a compiler flag can find, and it only helps when the macro declares a name.
In a header prefer static inline: a bare inline definition still needs exactly one translation unit to supply an external definition, and forgetting that produces a link error rather than a compile error.
Common mistakes
Giving a macro an internal temporary with a common name such as tmp, i, or n: the first caller that uses that name gets the macro operating on the caller's variable, and it compiles silently unless -Wshadow is on.
Expecting a macro to convert its arguments the way a prototype does: ((x) / 2) on an int truncates with no warning, while a double parameter would have widened the argument at the call.
Passing a function-like macro where a function pointer is wanted, as in qsort(v, n, sizeof v[0], CMP): the name is not followed by ( so it is never expanded, and the error reads as an undeclared identifier on a line that looks fine.
Try it yourself
Change, predict, then run
Write #define AVG3(a, b, c, out) do { int sum = (a) + (b) + (c); (out) = sum / 3; } while (0) and call it as AVG3(1, 2, 4, sum) from a main whose own variable is named sum, then print sum. Rewrite it as static double avg3(double a, double b, double c) and name the two separate bugs that disappear.
Open the C workspaceCheck your understanding
Which of these macros cannot be replaced by an ordinary C function without changing what the caller gets?
- #define SQUARE(x) ((x) * (x))
- #define DEG_TO_RAD(d) ((d) * 3.14159265 / 180)
- #define ARRAY_LEN(a) (sizeof (a) / sizeof (a)[0])
- #define IS_VOWEL(c) ((c) == 'a' || (c) == 'e' || (c) == 'i' || (c) == 'o' || (c) == 'u')
Show answer
Array-to-pointer conversion happens at the call, so a function only ever receives int * and sizeof reports the pointer size; the macro expands while the argument is still an array, so the length survives. SQUARE is the tempting answer because it looks type-generic, but a static inline int square(int) (plus one per type you actually use) compiles to the same instructions and gets its arguments checked.