Lesson 04 / 13
The Preprocessor
The preprocessor's four different directives — macro value, conditional body, token pasting, inclusion — are in fact four appearances of a single job: text substitution. Because a macro knows no types, it produces the same expression so that it gives the correct result with two different types; but because it can substitute its argument more than once, it can run a call with a side effect a different number of times than a real function would.
Published Updated
In this lesson
- Defines the shared mechanism (text substitution) of the preprocessor's four different directives
- Proves that a macro knows no types by showing that the same definition works with two different types
- Shows with an example that a macro argument can be substituted more than once, and the risk this creates
Contents
The previous lesson opened up the mechanism behind the header text row and showed that the
preprocessor copies a file, but the rest of the source stays unaware of this copying. This
lesson carries the same observation to the preprocessor’s other three decisions — macro value,
conditional body, token pasting — and reveals that all four are in fact the appearance of a
single job: replacing a text in the source with another text, without the compiler ever
knowing.
These four decisions were seen one by one since the course’s first lesson: the FACTOR text,
the #ifdef VERBOSE choice, the PASTE(add, _three) name, the copying of stage.h. In that
lesson all four appeared side by side, as part of a single run. This lesson returns to each one
separately and shows their shared ground: all four work on pure text, without the compiler ever
getting involved.
All Four Are Text Substitution
All the preprocessor’s directives do a single job: replace a text in the source with another text.
| Directive | What it replaces, with what |
|---|---|
#define NAME value |
Every place NAME appears, with value |
#define NAME(x) body |
The call NAME(actual), with body with its xs replaced by actual |
a##b |
Two tokens, with a single new token |
#ifdef / #if |
A block of code, with either itself or empty text |
#include "d.h" |
One line, with the entire content of the file |
All five fall into the same class, because all five change the source’s text without the compiler getting involved. The only difference among them is what they replace with what — a name, a call, two tokens, a block of code, or an entire file.
This shared ground has a consequence: for the compiler, the text the preprocessor produces is indistinguishable from text hand-written in the source. The compiler does not know, and has no need to know, whether a line came from a macro, was copied from a header, or was written directly — whatever text is in front of it, it compiles that. This invisibility is the source of both the preprocessor’s power and its limits: on one hand it can produce source in repetitive forms a programmer would never write by hand, on the other it does not itself check the correctness of the text it produces.
The table’s fifth row — #include — was the entire subject of the previous lesson, but there it
was seen as a sharing tool. Here the same directive is looked at as a member of the substitution
family: the line #include "stage.h" puts another text in its own place, just like a macro
call. The only difference is scale — a macro replaces a few tokens, while an #include replaces
an entire file. From the preprocessor’s point of view there is no difference between the two;
both are applications, at different scales, of the same mechanism: reading a text and replacing
it with another text.
A Macro Knows No Types
A macro is not a function even if it takes a parameter; here a parameter declares no type, it is only a placeholder showing where it will be pasted inside the body. This is why the same macro can produce a correct result with arguments of different types. A real C function cannot behave this way: every parameter of a function is declared with a single type, and the argument given at the call is converted to that type — the conversion is sometimes silent, sometimes lossy. A macro’s parameter is entirely exempt from this conversion, because there is no type to convert to begin with.
// compute.c — the same macro is called with two different types
#include <stdio.h>
#define SQUARE(x) ((x) * (x))
int main(void) {
int whole = SQUARE(4);
double fractional = SQUARE(2.5);
printf("whole: %d\n", whole);
printf("fractional: %.2f\n", fractional);
return 0;
}
whole: 16 fractional: 6.25
LC9. SQUARE(4) turns at the preprocessor into the text ((4) * (4)), and SQUARE(2.5)
into the text ((2.5) * (2.5)) — both were produced from the same body, only the single token
substituted for x changing. While making this substitution the preprocessor never knows that
4 is an integer and 2.5 is a floating-point number; what it sees is only two separate pieces
of text. The compiler decides whether the result will be int or double — but this decision
is made after the macro’s expansion, exactly as if ((4) * (4)) and ((2.5) * (2.5)) had been
hand-typed into the source.
This also explains why SQUARE takes no type parameter: it cannot, because it has no concept of
type to begin with. The same body fits every type, because it was written specific to no type at
all — it was only pasted into text.
A real function cannot do the same job — at least not with a single definition. If
int square(int x) is written, it works only with int arguments; a call resembling
SQUARE(2.5) would silently truncate 2.5 to int and give the square of 2, which is 4.
The SQUARE macro giving a correct result with the same body for both int and double is
therefore not a convenience but the direct consequence of type not existing at all at the
preprocessor stage. The same flexibility carries a cost: a macro cannot check what type its
argument will be, because there is no type system to check with — even if SQUARE("text") is
written, the preprocessor does not reject it, it only produces the text (("text") * ("text"))
and hands the problem over to the compiler.
Conditional Body: One Step Further
The previous lessons chose the conditional body by looking only at whether a name is defined
(#ifdef). #if goes one step further than this: it computes the value of an integer constant
expression at the preprocessor. This shows that the preprocessor does not have an entirely
empty grasp — while reading the text it can also carry out some simple computations, but this
computation is a far narrower arithmetic than the compiler’s rich type system.
// threshold.c — #if evaluates an integer constant expression at the preprocessor
#include <stdio.h>
#define THRESHOLD 5
#if THRESHOLD > 3
static const char *result = "threshold greater than 3";
#else
static const char *result = "threshold less than or equal to 3";
#endif
int main(void) {
printf("%s\n", result);
return 0;
}
threshold greater than 3
LC10. Although #if THRESHOLD > 3 carries the same syntax as the place where the compiler
uses the > operator, it does not use the same calculating machine. The preprocessor has its
own arithmetic: every integer is widened to a single broad type, there is no floating-point
number at all, and an undefined name is read as 0. THRESHOLD‘s value first expands to the
text 5, then 5 > 3 is computed by the preprocessor’s own arithmetic, and the result is
reduced to true or false. The compiler never sees this computation; what reaches it is only the
branch chosen — here the line
static const char *result = "threshold greater than 3";.
This distinction also clarifies the difference between #ifdef and #if. #ifdef NAME looks
at whether NAME is defined — it never looks at its value; #ifdef NAME is true even if NAME
is defined as 0. #if NAME, in the exact opposite way, computes NAME‘s value with the
preprocessor’s own arithmetic; #if NAME is false if NAME is 0. Confusing these two
directives in a source can silently select the wrong branch while working with a constant that
is defined but zero — this lesson does not use these two directives in place of one another, it
chooses knowing the difference between them.
Thanks to its own arithmetic, #if can do one more thing: combine multiple conditions in a
single line with &&, ||, and comparison operators, or use #elif to choose the first true
one among alternatives listed in sequence. #ifdef cannot do this — it can only ask whether a
single name is defined, it cannot ask which of two defined names corresponds to a larger number.
This shows once more why the two directives do not substitute for one another: one asks about
existence, the other computes a value.
Token Pasting: Different Names from the Same Body
In the previous lesson ## was seen in a single example: it pasted two fixed tokens (add and
_three) together and produced a single name. Its real power was not visible there, because
that call happened only once. ##’s real power is that the same macro body can produce
different names in different calls — a macro body, written only once in the source and called
many times, can turn into a new and independent name each time.
// generate.c — the same macro body turns into two different functions with two different calls
#include <stdio.h>
#define MAKE_FUNCTION(name) int make_##name(int x) { return x + 1; }
MAKE_FUNCTION(a)
MAKE_FUNCTION(b)
int main(void) {
printf("make_a(5) = %d\n", make_a(5));
printf("make_b(5) = %d\n", make_b(5));
return 0;
}
make_a(5) = 6 make_b(5) = 6
LC11. The line MAKE_FUNCTION(a) turns at the preprocessor into the text
int make_a(int x) { return x + 1; }, and the line MAKE_FUNCTION(b) into the text
int make_b(int x) { return x + 1; }. The compiler never sees a name called MAKE_FUNCTION;
what it sees in the source are two separate function definitions, both standing as if
hand-written. The same macro body can produce as many different functions as wanted by changing
its argument — but each produced name goes to the compiler as a single piece of text along with
the rest of the body; no link, no “came from the same macro” information, is carried between
them.
PASTE(add, _three) in the course’s first lesson did the same job, but with a single call. The
difference here is that the same generating body can be called more than once —
MAKE_FUNCTION was used twice and two different functions were born. Had a third line,
MAKE_FUNCTION(c), been added to the source, the compiler would see a third function as well;
the amount of text token pasting produces depends on how many times the macro is called, not on
the length of the source.
The Bounding Measurement
This lesson’s four sections all showed that a macro knows no types, that a conditional body
decides with its own arithmetic, that token pasting can produce new identifiers — the three of
them together were telling of the preprocessor’s power. This final section shows a cost of that
same power. A macro is not a function; its argument can be substituted more than once. In
SQUARE(x)’s body, x appears twice — this means that if the argument is an expression with a
side effect, it will be run twice. In a real function that does the same work, the argument is
always computed once.
// compare.c — the macro runs its argument twice, the function runs its argument once
#include <stdio.h>
#define SQUARE(x) ((x) * (x))
static int counter = 0;
static int increment(void) {
counter++;
return counter;
}
static int square(int x) {
return x * x;
}
int main(void) {
counter = 0;
int macro_result = SQUARE(increment());
int macro_counter = counter;
counter = 0;
int function_result = square(increment());
int function_counter = counter;
printf("macro: result %d, counter incremented %d times\n", macro_result, macro_counter);
printf("function: result %d, counter incremented %d times\n", function_result, function_counter);
return 0;
}
macro: result 2, counter incremented 2 times function: result 1, counter incremented 1 times
LC12. SQUARE(increment()) turns at the preprocessor into the text
((increment()) * (increment())) — the call increment() is now written twice in the source.
While the program runs, both of these calls actually happen: counter starts at zero, rises
first to 1, then to 2, and the product gives 1 * 2 = 2. Which call runs first depends on
the compiler, and this lesson makes no claim about that — the only claim made is that both calls
happen and counter is incremented twice. In the square call, on the other hand,
increment() is written only once and runs only once: the argument is computed first, its
result is passed to square once, and x, however many times it appears in the body, is now a
fixed value.
This difference shows why calling a macro like SQUARE with an argument that has a side effect
is risky: the macro’s writer knows how many times the argument will be used by looking at the
body, but the caller can call it like an ordinary function without seeing this. SQUARE‘s name
and manner of use are the same as a function’s, but its contract is different — the argument
must be an expression free of side effects.
The parentheses around (x) in SQUARE(x)‘s body have nothing to do with this risk; they solve
a separate problem. A body without parentheses (x * x) would expand SQUARE(1 + 2) into the
text 1 + 2 * 1 + 2; because multiplication is processed before addition, this expression gives
1 + 2 + 2 = 5, whereas the expected result — the square of 3 — is 9. Multiplication’s
precedence mistakes the pieces of the addition for its own argument. The parenthesized body
prevents this: ((1 + 2) * (1 + 2)) first evaluates each of the two parenthesized groups as its
own whole, then multiplies, and the result correctly comes out as 9. But these parentheses do
not change how many times the argument is written — ((1 + 2) * (1 + 2)) still carries 1 + 2
twice. Both problems come from the same root — the argument interacts with what surrounds it
when it is substituted into the text — but their solutions are separate: parentheses preserve
the order of precedence, while keeping the argument free of side effects is the caller’s
responsibility.
Summary
- All the preprocessor’s directives do a single job: replacing a text in the source with another text; macro value, conditional body, token pasting, and inclusion are four appearances of this.
- A macro knows no types: the same body, with arguments of different types, can produce a correct result while the preprocessor’s work never changes.
#ifcomputes an integer constant expression with the preprocessor’s own arithmetic; this arithmetic is not the same as the compiler’s type system.- The same macro body can produce different names in different calls through token pasting; the compiler does not know these names come from a common source.
- A macro can substitute its argument more than once; an argument with a side effect can therefore run a different number of times than it would with a real function.
- Parentheses in a macro body preserve the order of precedence, but do not change how many times the argument is written — two separate solutions to two separate problems.
Next Step
Through these four lessons, no code block left behind a single warning — but this was not a coincidence, it was a consistency provided by hand: every macro was carefully parenthesized, every conditional branch was kept at a consistent type, every token pasting resulted in a valid identifier. The preprocessor checks none of this consistency; what checks it is the stage right after it. The next lesson looks at this checking itself: which stage a warning is born at, what a compiler warning flag makes visible, and which behavior the standard guarantees and which it leaves to the implementation. This topic’s last lesson returns here to the question this course asked at its opening: how each of the four stages makes visible the error that falls to it.
To keep your progress and take notes, Log in
My notes
Log in to take notes.