Skip to content
academia.sh

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.
  • #if computes 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.

Start typing to search.

↑↓ ↵ Esc navigate · open · close