---
title: 'The Preprocessor'
source: 'https://academia.sh/en/courses/c-fundamentals/the-preprocessor'
course: 'C Fundamentals'
language: en
updated: '2026-09-26T03:02:42+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - "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'
---

# 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.

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 `x`s 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.

```c
// 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.

```c
// 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.

```c
// 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.

```c
// 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.
