8. The Preprocessor
Before the compiler ever sees your code, a separate program rewrites it: the preprocessor pastes in headers, expands macros, and deletes the branches you told it to skip. It is a pure text transformer with no knowledge of C's types, scopes, or syntax, which makes it both remarkably powerful and uniquely dangerous. This chapter covers the directives you use daily, the conditional compilation that makes portable code possible, and the macro techniques — and traps — that separate robust codebases from ones nobody can debug.
<a id="81-directives"></a>
8.1 Directives
Preprocessor directives are lines beginning with # that instruct the preprocessor rather than the compiler. This section covers the four you will use constantly — #include, #define for constants and for macros, and #undef — plus the macros the implementation defines for you.
#include and Search Paths
Theory
C has no module system. There is no import that understands packages, no compiled interface file the compiler consults. Instead there is #include, and what it does is almost comically simple: it replaces the directive line with the entire contents of the named file, then continues preprocessing that text as though you had typed it.
That literal substitution explains everything about how C headers behave. A header is not "imported" — it is pasted. If two files include the same header, both get a full copy of its text. If a header includes another header, you get both, expanded in place. The compiler never learns that a file called stdio.h exists; it just sees several thousand lines of declarations at the top of your translation unit.
There are two spellings, and the difference is about where the preprocessor looks:
#include <stdio.h>— angle brackets. Search the system include directories: the compiler's built-in paths (/usr/include,/usr/local/include, the toolchain's own directory) plus anything added with-I. Use this for the standard library and third-party libraries installed on the system.#include "myheader.h"— quotes. Search the directory of the file doing the including first, then fall back to the entire angle-bracket search path. Use this for headers belonging to your own project.
The rule of thumb is ownership: quotes for your code, angle brackets for everyone else's.
Because a header gets pasted wherever it is named, including one twice in a single translation unit duplicates every declaration in it — harmless for function prototypes, but a hard error for struct and enum definitions, which may not be repeated. Since headers include other headers, this happens constantly and by accident. The fix is an include guard: wrap the whole header in a conditional keyed on a unique macro, so the second and subsequent inclusions expand to nothing.
#ifndef PROJECT_MODULE_H /* first time: not defined, so continue */
#define PROJECT_MODULE_H /* mark it, so next time we skip */
/* ... contents ... */
#endif
Every header you write needs one. The near-universal alternative, #pragma once, is a single line supported by every mainstream compiler, but it is not in the C standard and it can misbehave when the same file is reachable through two paths (symlinks, bind mounts, hard links) since it identifies files by inode or path rather than by name. Include guards are the portable choice; many projects use both.
Two further disciplines pay off in real codebases. Headers should be self-contained: a header must include whatever it needs so that including it first, alone, compiles. And include what you use: do not rely on a header transitively dragging in <stddef.h> for you, because the day it stops doing so your build breaks for no visible reason.
Examples
/* ---------- geometry.h : a well-formed header ---------- */
#ifndef GEOMETRY_H /* include guard: unique name, often PATH_BASED */
#define GEOMETRY_H
#include <stddef.h> /* self-contained: we use size_t below */
typedef struct { double x, y; } Point; /* CANNOT be repeated: needs the guard */
double point_distance(Point a, Point b); /* declarations may repeat harmlessly */
size_t point_hash(Point p);
#endif /* GEOMETRY_H */
/* ---------- main.c ---------- */
#include <stdio.h> /* angle: system/library headers */
#include <math.h>
#include "geometry.h" /* quotes: this project's headers */
#include "geometry.h" /* included AGAIN -- the guard makes it a no-op */
int main(void)
{
Point a = {0.0, 0.0}, b = {3.0, 4.0};
printf("distance = %.1f\n", point_distance(a, b));
return 0;
}
Watching the substitution happen makes the mental model concrete:
# -E stops after preprocessing: no compilation, just the expanded text.
$ gcc -E main.c | wc -l
1247 # our 12-line file became 1247 lines
# See exactly which files were pulled in, and how deeply nested:
$ gcc -H -fsyntax-only main.c
. /usr/include/stdio.h
.. /usr/include/x86_64-linux-gnu/bits/types.h
... /usr/include/x86_64-linux-gnu/bits/wordsize.h
. /usr/include/math.h
. geometry.h # one dot per nesting level
# Add a search directory for both forms:
$ gcc -Iinclude -Ivendor/lib/include main.c
WHY GUARDS ARE NOT OPTIONAL
main.c
|
+-- #include "window.h" ---> #include "types.h" --> typedef ... Point;
|
+-- #include "canvas.h" ---> #include "types.h" --> typedef ... Point;
^
|
Second expansion redefines Point.
Without a guard: "error: redefinition of 'Point'"
With a guard: the second expansion is empty.
#include <file.h> | #include "file.h" | |
|---|---|---|
| Searches first | system directories and -I paths | the including file's own directory |
| Falls back to | nothing further | the full system/-I search path |
| Intended for | standard library, installed third-party | headers inside your project |
Key Takeaways
#includeperforms literal text substitution — the header's full contents replace the directive line, with no notion of modules or importing.- Angle brackets search system and
-Idirectories; quotes search the including file's directory first, then everything else. - Every header needs an include guard (
#ifndef/#define/#endif); repeated type definitions are an error, and nested includes make repetition inevitable. #pragma onceis shorter and universally supported in practice, but non-standard and confusable by symlinks and duplicate paths.- Make headers self-contained and include what you use, so a change in someone else's header cannot break your build.
🧪 Practice
- Write a header defining a struct without a guard, include it twice from one
.cfile, and read the error. Then add the guard and confirm it compiles.- Run
gcc -Eon a file containing only#include <stdio.h>andint main(void){return 0;}and count the resulting lines. Then rungcc -Hand identify the three most deeply nested headers.- Create
include/util.hand compile a file that includes it as"util.h"with and without-Iinclude. Explain both outcomes.- Interview-style: "What is the practical difference between
#include <x.h>and#include "x.h"?" Hint: describe the search order, and say what happens when the quoted form fails to find the file locally.
#define Object-Like Macros
Theory
An object-like macro gives a name to a piece of replacement text. Wherever the name appears as a separate token afterwards, the preprocessor substitutes the text.
#define MAX_USERS 100
From that line onward, every MAX_USERS token becomes 100 before the compiler sees anything. The compiler never hears the name; error messages will talk about 100.
The historical motivation was that C had no other way to write a compile-time constant usable in contexts that demand one — array sizes at file scope, case labels, bit-field widths. A const int variable in C is not a constant expression (unlike in C++), so int buf[MAX]; with const int MAX = 100; is a VLA at best and an error at file scope. Macros filled that gap.
Three properties define how they behave, and each one is a source of surprise:
The replacement is textual, not evaluated. #define AREA 3 + 4 makes AREA * 2 expand to 3 + 4 * 2, which is 11, not 14. The macro does not compute anything; it pastes tokens, and the surrounding operators then bind however C's precedence dictates. Always parenthesize a replacement that is an expression: #define AREA (3 + 4).
There is no type and no scope. A macro is not a variable. It ignores blocks and functions entirely, is invisible to the debugger, and lives from its #define to the end of the file (or a #undef). This is why macro names are conventionally SHOUTING_CASE — the convention is the only warning a reader gets, and it also avoids colliding with ordinary identifiers.
The definition ends at the newline. Continue a long macro with a backslash at end of line. And note there is no semicolon: #define N 100; puts the semicolon into the expansion, so int a[N]; becomes int a[100;];.
Since C99 and C11, macros have real competition for this job. enum constants are genuine integer constant expressions, are typed, are scoped, and are visible to the debugger — better than a macro for related integers. const objects work wherever a constant expression is not required, and carry a type the compiler can check. static const at file scope is the idiomatic choice for a floating-point constant. The remaining unambiguous macro territory is: integer constants needed in constant-expression contexts, string literals you want concatenated at compile time, and configuration values you want overridable from the build system with -D.
Examples
#include <stdio.h>
/* --- good object-like macros ---------------------------------------- */
#define MAX_USERS 100
#define PI 3.14159265358979323846
#define GREETING "Hello, "
#define APP_NAME "demo"
#define BANNER GREETING APP_NAME /* literals concatenate */
/* Parenthesize any replacement that is an expression. */
#define BAD_SUM 3 + 4 /* dangerous */
#define GOOD_SUM (3 + 4) /* safe */
/* A long definition continued with backslashes (no trailing backslash
on the final line, and nothing after any backslash -- not even a space). */
#define VERSION_STRING "v" "1" "." "4" "." \
"2"
/* Overridable from the build: -DBUFFER_SIZE=8192 wins over this default. */
#ifndef BUFFER_SIZE
# define BUFFER_SIZE 1024
#endif
int global_buffer[MAX_USERS]; /* a macro works where a const int cannot */
int main(void)
{
printf("%s %s\n", BANNER, VERSION_STRING);
printf("buffer size: %d\n", BUFFER_SIZE);
/* The textual-substitution trap, demonstrated. */
printf("BAD_SUM * 2 = %d (expands to 3 + 4 * 2)\n", BAD_SUM * 2);
printf("GOOD_SUM * 2 = %d (expands to (3 + 4) * 2)\n", GOOD_SUM * 2);
/* Macros have no scope: this one is live from here to end of file. */
#define LOCAL_LOOKING 5
{
int x = LOCAL_LOOKING; /* the braces mean nothing to it */
printf("x = %d\n", x);
}
printf("still visible: %d\n", LOCAL_LOOKING);
double area = PI * 2.0 * 2.0;
printf("area = %.4f\n", area);
printf("array capacity = %zu\n", sizeof global_buffer / sizeof *global_buffer);
return 0;
}
WHAT THE COMPILER ACTUALLY RECEIVES
you write: preprocessor emits:
------------------------ ------------------------------
BAD_SUM * 2 3 + 4 * 2 -> 11 (surprise)
GOOD_SUM * 2 (3 + 4) * 2 -> 14 (intended)
int a[MAX_USERS]; int a[100];
BANNER "Hello, " "demo" -> one literal
| Mechanism | Constant expression? | Typed | Scoped | Debugger sees it |
|---|---|---|---|---|
#define N 100 | yes | no | no | no |
enum { N = 100 }; | yes | int | yes | yes |
const int N = 100; | no (in C) | yes | yes | yes |
static const double X = 1.5; | no | yes | yes | yes |
Key Takeaways
- An object-like macro substitutes text; it does not evaluate, has no type, and obeys no scope or block structure.
- Parenthesize any replacement that is an expression, or surrounding operators will bind in ways you did not intend.
- Never end a macro definition with a semicolon, and continue long definitions with a trailing backslash.
- Prefer
enumfor related integer constants andconstwhere a constant expression is not required — both are typed, scoped, and debuggable. - Reserve macros for constant-expression contexts, string-literal building, and values the build system overrides with
-D.
🧪 Practice
- Define
#define SQUARE_SIDE 2 + 3and printSQUARE_SIDE * SQUARE_SIDE. Predict the value first, then fix it with parentheses.- Define a constant three ways (
#define,enum,const int) and try to use each as an array size at file scope. Explain which fail and why.- Add
#ifndef-guarded defaults for two configuration macros, then override one with-Don the command line and verify withgcc -E.- Interview-style: "Why is
const int N = 10;not usable as an array size at file scope in C?" Hint: consider the exact definition of "integer constant expression" and how C differs from C++ here.
Function-Like Macros
Theory
A macro can take parameters:
#define SQUARE(x) ((x) * (x))
SQUARE(5) expands to ((5) * (5)). This looks like a function call and is often described as one, but the resemblance is superficial and the differences cause most macro bugs.
A function-like macro is still pure text substitution. The arguments are token sequences, not values. They are not evaluated before substitution, they are not converted to a parameter type, and each one is pasted in wherever the parameter name appears — possibly several times, possibly zero times. There is no call, no stack frame, and no return value; the expansion simply becomes part of the surrounding expression.
Three rules follow directly, and skipping any of them produces a macro that works in testing and fails in production:
Parenthesize every parameter, every time it appears. Without it, an argument that is an expression gets torn apart by the surrounding operators. #define SQUARE(x) x * x turns SQUARE(a + b) into a + b * a + b.
Parenthesize the entire replacement. Without it, the macro's own result gets torn apart by operators around the call site. #define DOUBLE(x) (x) + (x) makes DOUBLE(3) * 2 expand to (3) + (3) * 2, which is 9.
Never use a parameter more than once if you can avoid it. Because arguments are pasted, MAX(i++, j) may increment i twice. This is the multiple-evaluation problem, serious enough that it gets its own topic in 8.3.
Two syntactic gotchas are worth naming. There must be no space between the macro name and the opening parenthesis in the definition: #define F (x) ... defines an object-like macro whose replacement text starts with (x). And a macro argument containing a top-level comma is split into two arguments, so ASSERT_EQ(f(a, b), c) is fine (the comma is inside parentheses) but DECLARE(struct { int a, b; }, name) is not — braces do not protect commas, only parentheses do.
Since C99, macros may be variadic: ... in the parameter list collects the remaining arguments, and __VA_ARGS__ expands to them. The classic problem — a trailing comma when zero variadic arguments are supplied — is solved by GNU's , ##__VA_ARGS__ extension or, portably since C23, by __VA_OPT__(,).
Given all this, when is a function-like macro the right tool? When you need something a function cannot do: operate on types rather than values, capture __FILE__/__LINE__ from the call site, produce a compile-time constant, or generate code (see X-Macros in 8.3). For anything that is genuinely a computation, a static inline function is better in every respect — it is type-checked, evaluates each argument once, and modern compilers inline it just as effectively.
Examples
#include <stdio.h>
/* --- correct: every parameter and the whole body parenthesized -------- */
#define SQUARE(x) ((x) * (x))
#define MAX(a, b) ((a) > (b) ? (a) : (b))
#define IS_EVEN(n) (((n) % 2) == 0)
/* --- broken versions, for contrast ---------------------------------- */
#define BAD_SQUARE(x) x * x /* no parameter parens */
#define BAD_DOUBLE(x) (x) + (x) /* no outer parens */
/* --- variadic macro (C99) ------------------------------------------- */
#define LOG(fmt, ...) \
fprintf(stderr, "[%s:%d] " fmt "\n", __FILE__, __LINE__, __VA_ARGS__)
/* GNU extension handles the zero-argument case by eating the comma. */
#define LOG_ANY(fmt, ...) \
fprintf(stderr, "[%s:%d] " fmt "\n", __FILE__, __LINE__, ##__VA_ARGS__)
/* --- what a macro can do that a function cannot --------------------- */
#define ARRAY_LEN(a) (sizeof (a) / sizeof (a)[0]) /* operates on a TYPE */
#define STRINGIFY(x) #x /* sees the TOKENS */
/* --- what a function does better ------------------------------------ */
static inline int square_fn(int x) { return x * x; } /* type-checked, one eval */
int main(void)
{
printf("SQUARE(5) = %d\n", SQUARE(5));
printf("SQUARE(2 + 3) = %d\n", SQUARE(2 + 3)); /* 25 */
printf("BAD_SQUARE(2 + 3) = %d <- expands to 2 + 3 * 2 + 3\n",
BAD_SQUARE(2 + 3)); /* 11 */
printf("SQUARE(3) * 2 = %d\n", SQUARE(3) * 2); /* 18 */
printf("BAD_DOUBLE(3) * 2 = %d <- expands to (3) + (3) * 2\n",
BAD_DOUBLE(3) * 2); /* 9 */
printf("MAX(10, 20) = %d\n", MAX(10, 20));
int nums[] = {1, 2, 3, 4, 5};
printf("ARRAY_LEN(nums) = %zu (a function cannot do this)\n",
ARRAY_LEN(nums));
printf("STRINGIFY(a + b) = \"%s\"\n", STRINGIFY(a + b));
LOG("started with %d items", 5);
LOG_ANY("no arguments at all");
printf("square_fn(7) = %d\n", square_fn(7));
return 0;
}
EXPANSION, STEP BY STEP
source: SQUARE(2 + 3) * 2
substitute: ((2 + 3) * (2 + 3)) * 2
compiler: 25 * 2 = 50 correct
source: BAD_SQUARE(2 + 3) * 2 #define BAD_SQUARE(x) x * x
substitute: 2 + 3 * 2 + 3 * 2
compiler: 2 + 6 + 6 = 14 silently wrong
The macro never "computes" anything. It hands the compiler a token
sequence, and C's precedence rules decide the meaning.
| Function-like macro | static inline function | |
|---|---|---|
| Argument type checking | none | full |
| Argument evaluation | once per textual use | exactly once |
| Can take a type as an argument | yes | no |
Sees __FILE__ / __LINE__ of the caller | yes | no |
| Debugger steps into it | no | yes |
| Result usable as a constant expression | yes | no |
Key Takeaways
- A function-like macro substitutes token sequences; there is no call, no type checking, and no evaluation before substitution.
- Parenthesize every parameter use and the entire replacement body — omitting either lets surrounding operators corrupt the meaning.
- No space is allowed between the macro name and
(in the definition, and top-level commas split arguments regardless of braces. - Variadic macros use
...and__VA_ARGS__; handle the empty case with##__VA_ARGS__(GNU) or__VA_OPT__(,)(C23). - Use macros only for what functions cannot do — type operations, call-site metadata, constant expressions, code generation — and prefer
static inlineotherwise.
🧪 Practice
- Write
#define CUBE(x) x * x * x, evaluateCUBE(1 + 1), explain the result, then fix it and verify withgcc -E.- Write
MIN(a, b)correctly, then call it asMIN(x++, y)and describe exactly what the expansion does tox.- Write a variadic
LOGmacro that prints file and line, and make it work with zero variadic arguments both the GNU way and the C23 way.- Interview-style: "Give a case where a function-like macro is strictly better than an inline function." Hint: think about what information disappears once the compiler has parsed a call, and about
sizeof.
#undef
Theory
#undef NAME removes a macro definition. From that line onward, NAME is an ordinary identifier again, and the preprocessor will not substitute anything for it.
Because macros ignore scope and persist to the end of the translation unit, #undef is the only way to bound one. That makes it the tool for a small set of specific jobs:
Limiting a helper macro's blast radius. A macro defined to generate a table or simplify a repetitive block should be undefined immediately after the block that needs it. Otherwise it leaks into every subsequent line of the file — and into every file that includes this one, if it lives in a header. A header that defines a macro named MIN and never undefines it will fight with everything else in the program.
Redefining a macro. Redefining a macro with different replacement text is a constraint violation (the compiler must diagnose it) unless the new definition is identical, token for token. #undef first, then define again, and it is legal.
Neutralizing an implementation's macro. Standard library functions are permitted to be implemented as macros as well as functions — getchar, putc, isdigit, and assert commonly are. Occasionally you need the real function: to take its address, or to defeat a macro that expands its argument twice. #undef getchar exposes the underlying function, since the standard guarantees one exists.
Disabling assert. <assert.h> is designed to be included repeatedly with NDEBUG toggled in between; it undefines and redefines assert each time.
#undef on a name that is not defined is explicitly legal and does nothing, so defensive #undef before #define is safe. One caution: you may not #undef names the standard reserves — defined, and, in strict terms, standard library macro names in headers you have included, though implementations tolerate most of it.
The overall guidance is simple: if a macro is a local implementation detail, undefine it as soon as you are done. If it is part of your module's public interface, name it with a project prefix so it cannot collide, and leave it defined.
Examples
#include <stdio.h>
#include <ctype.h>
int main(void)
{
/* --- bounding a helper macro's lifetime -------------------------- */
#define ROW(label, value) printf("%-10s %d\n", (label), (value));
ROW("alpha", 1)
ROW("beta", 2)
ROW("gamma", 3)
#undef ROW /* the helper cannot leak past this point */
/* ROW("delta", 4); would now be an "undeclared" error */
/* --- redefinition requires an intervening #undef ----------------- */
#define LIMIT 100
printf("LIMIT = %d\n", LIMIT);
/* #define LIMIT 200 <- ERROR: incompatible redefinition */
#undef LIMIT
#define LIMIT 200 /* legal now */
printf("LIMIT = %d\n", LIMIT);
/* --- getting the real function behind a library macro ------------ */
printf("isdigit('7') via macro = %d\n", isdigit('7'));
#undef isdigit /* the standard guarantees a real function too */
printf("isdigit('7') via func = %d\n", isdigit('7'));
/* A function pointer requires the function, not the macro. */
int (*classify)(int) = isdigit;
printf("through a pointer = %d\n", classify('7'));
/* --- #undef of an undefined name is legal and does nothing ------- */
#undef NEVER_DEFINED_ANYWHERE
return 0;
}
A header that defines a helper for its own declarations must clean up after itself:
/* ---------- events.h ---------- */
#ifndef EVENTS_H
#define EVENTS_H
/* A local generator macro -- an implementation detail of this header. */
#define DECLARE_EVENT(name) void on_##name(int code);
DECLARE_EVENT(open) /* void on_open(int code); */
DECLARE_EVENT(close) /* void on_close(int code); */
DECLARE_EVENT(error) /* void on_error(int code); */
#undef DECLARE_EVENT /* ESSENTIAL: do not export it to every includer */
#endif /* EVENTS_H */
MACRO LIFETIME IN A TRANSLATION UNIT
line 1 ................................ ROW undefined
line 12 #define ROW(...) ------+
line 14 ROW(alpha, 1) | ROW is live here
line 16 ROW(gamma, 3) |
line 18 #undef ROW ------+
line 20 ................................ ROW undefined again
EOF
Without the #undef, the bar extends to EOF -- and, from a header,
into every file that includes it.
Key Takeaways
#undef NAMEends a macro's definition; it is the only way to bound a macro, which otherwise lives to the end of the translation unit.- Redefining a macro with different text is an error unless you
#undefit first; an identical redefinition is permitted. - Always
#undefhelper macros defined inside a header, or they leak into every file that includes it. - Standard library facilities may be macros;
#undefexposes the guaranteed real function, which you need to take its address. #undefon an undefined name is legal and harmless, so a defensive#undefbefore#definecosts nothing.
🧪 Practice
- Define a macro, use it,
#undefit, then use it again and read the error message. Explain what the compiler now thinks the name is.- Try to redefine a macro with different replacement text without
#undef, then with it, and compare the diagnostics.- Write a header that uses a generator macro to declare five functions and undefines it at the end; verify with
gcc -Ethat the macro is gone from the output.- Interview-style: "Why might you
#undefa standard library name likegetchar?" Hint: consider what the standard permits an implementation to do, and what you cannot do with a macro.
Predefined Macros
Theory
The preprocessor defines a set of macros for you. They are the only way your source code can learn about itself — where a line came from, when it was built, which standard and compiler are in use — and they are the raw material of assertions, logging, and portable code.
The standard mandates a small, reliable set:
| Macro | Expands to |
|---|---|
__FILE__ | the current source file name, as a string literal |
__LINE__ | the current line number, as an integer constant |
__DATE__ | compilation date, "Mmm dd yyyy" |
__TIME__ | compilation time, "hh:mm:ss" |
__STDC__ | 1 in a conforming implementation |
__STDC_VERSION__ | the standard revision, e.g. 201112L for C11 |
__STDC_HOSTED__ | 1 for a hosted implementation, 0 for freestanding |
C99 added __func__, which is worth a special note: it is not a macro. It is a predefined identifier — a static const char[] holding the enclosing function's name, declared implicitly at the top of every function body. The practical consequence is that it does not concatenate with adjacent string literals the way __FILE__ does, so "in " __func__ fails to compile while "in " __FILE__ works.
Compilers and platforms add many more, and this is what makes conditional compilation possible: __GNUC__, __clang__, _MSC_VER identify the compiler; __linux__, _WIN32, __APPLE__ identify the OS; __x86_64__, __aarch64__ identify the architecture. These are not standardized, so relying on them is inherently a portability decision — covered properly in 8.2.
__LINE__ and __FILE__ are the reason certain facilities must be macros. assert(x) reports the file and line of the failing assertion, not of the assert implementation, because the macro captures them at the expansion site. A function receiving x could never know where it was called from. The same trick underlies every serious logging macro.
Two practical warnings. __DATE__ and __TIME__ embed the build timestamp, which destroys reproducible builds — the same source produces different binaries — so many projects ban them or override them with a fixed value. And you can query everything a compiler defines, which is far more useful than memorizing lists:
$ gcc -dM -E - < /dev/null | sort | head
Examples
#include <stdio.h>
/* __FILE__ and __LINE__ are captured where the macro is EXPANDED,
which is exactly why this cannot be a function. */
#define LOG(msg) \
printf("%s:%d in %s(): %s\n", __FILE__, __LINE__, __func__, (msg))
#define TRACE_HERE() \
printf("reached %s:%d\n", __FILE__, __LINE__)
static void worker(void)
{
LOG("inside worker"); /* reports worker's line, not LOG's */
}
int main(void)
{
printf("file : %s\n", __FILE__);
printf("line : %d\n", __LINE__);
printf("function : %s\n", __func__); /* an identifier, not a macro */
printf("built : %s %s\n", __DATE__, __TIME__);
/* __FILE__ is a string literal, so it concatenates: */
puts("compiled from " __FILE__);
/* puts("inside " __func__); <- ERROR: __func__ is not a literal */
#ifdef __STDC_VERSION__
printf("standard : %ldL\n", __STDC_VERSION__);
#else
printf("standard : C89/C90\n");
#endif
#if defined(__clang__)
printf("compiler : clang %d.%d\n", __clang_major__, __clang_minor__);
#elif defined(__GNUC__)
printf("compiler : gcc %d.%d\n", __GNUC__, __GNUC_MINOR__);
#elif defined(_MSC_VER)
printf("compiler : MSVC %d\n", _MSC_VER);
#else
printf("compiler : unknown\n");
#endif
TRACE_HERE();
worker();
return 0;
}
Typical output:
file : macros.c
line : 20
function : main
built : Sep 8 2026 14:02:11
compiled from macros.c
standard : 201710L
compiler : gcc 13.3
reached macros.c:41
macros.c:14 in worker(): inside worker
| Standard version | __STDC_VERSION__ |
|---|---|
| C89 / C90 | not defined |
| C95 (amendment 1) | 199409L |
| C99 | 199901L |
| C11 | 201112L |
| C17 / C18 | 201710L |
| C23 | 202311L |
# List every macro your compiler predefines (hundreds of them):
$ gcc -dM -E - < /dev/null | wc -l
396
$ gcc -dM -E - < /dev/null | grep -E '__(linux|x86_64|GNUC)__'
#define __linux__ 1
#define __x86_64__ 1
#define __GNUC__ 13
# Override the build timestamp for reproducible builds:
$ SOURCE_DATE_EPOCH=1700000000 gcc prog.c -o prog
Key Takeaways
__FILE__,__LINE__,__DATE__,__TIME__,__STDC__, and__STDC_VERSION__are standard and always available in a conforming implementation.__func__is a predefined identifier, not a macro, so it cannot be concatenated with string literals.- Capturing
__FILE__and__LINE__at the expansion site is precisely whyassertand logging facilities must be macros. - Compiler, OS, and architecture macros (
__GNUC__,__linux__,_WIN32,__x86_64__) are implementation-defined but essential for portable code. __DATE__and__TIME__break reproducible builds; usegcc -dM -E - < /dev/nullto discover what a toolchain actually defines.
🧪 Practice
- Write a
LOGmacro using__FILE__,__LINE__, and__func__, call it from three different functions, and confirm each reports its own location.- Try to write
puts("in " __func__);andputs("in " __FILE__);. Explain why exactly one compiles.- Run
gcc -dM -E - < /dev/nulland find the macros identifying your OS, architecture, compiler version, and pointer size.- Interview-style: "Why must
assertbe a macro rather than a function?" Hint: think about what the failure message must contain and where that information exists.
<a id="82-conditional-compilation"></a>
8.2 Conditional Compilation
Conditional directives let the preprocessor delete regions of code before the compiler sees them, which is how one source tree builds for several platforms, standards, and configurations. This section covers the conditional directives themselves, the feature test macros that gate library functionality, platform detection, debug versus release builds, and the diagnostic directives.
#if, #ifdef, #ifndef, #else, #elif
Theory
Conditional compilation answers a question if cannot: should this code exist at all? A run-time if requires every branch to compile — every function it calls must be declared, every type must be defined, every symbol must link. That is impossible when one branch calls a Windows API and the other calls a POSIX one, or when a branch uses a C11 feature on a C89 compiler.
Conditional directives operate a step earlier. The preprocessor evaluates the condition and passes only the surviving branch to the compiler. The other branch is deleted as text: it may reference undeclared functions, use unsupported syntax, or be complete nonsense for the current target, and nothing will complain. It is not compiled, so it does not have to be compilable.
The family:
#ifdef NAME— true ifNAMEis defined as a macro, regardless of its value.#ifndef NAME— true if it is not. The basis of include guards.#if expr— true if the constant integer expressionexpris non-zero.#elif expr,#else,#endif— chain and terminate.defined(NAME)— an operator usable inside#if, so you can combine tests:#if defined(A) && !defined(B).
The #if expression evaluator has its own rules, and they are a common source of confusion:
- It handles integer arithmetic only — no floating point, no
sizeof, no casts, no enum constants (those are compiler-level, and the preprocessor runs first), no string comparison. - Any identifier that is not a defined macro becomes
0. This is silent and is the single biggest hazard: a typo in#if VERSON > 2evaluates as0 > 2and quietly takes the wrong branch with no diagnostic. Use#if defined(X) && X > 2when the distinction matters, and turn on-Wundefso the compiler warns about undefined identifiers in#if. - Arithmetic uses the widest integer type available, and
#if 1/0is an error, not undefined behaviour.
Choosing between the forms: use #ifdef/#ifndef when only existence matters (include guards, feature presence). Use #if when a value matters, or when you need boolean combinations — #ifdef cannot express &&. Note also that #if 0 ... #endif is the correct way to comment out a block of code that itself contains comments, since C comments do not nest. The skipped text still has to be tokenizable — an unterminated quote or comment inside it is an error — but it need not be valid C.
Finally, keep in mind that conditionals multiply. Three independent two-way conditionals produce eight possible builds, of which your CI probably compiles one. Code inside an unusual branch rots silently. The practical mitigation is to isolate platform differences behind a small internal interface implemented once per platform, rather than sprinkling #ifdef through the logic.
Examples
#include <stdio.h>
#define FEATURE_LOGGING 1
#define MAX_CONNECTIONS 50
/* VERBOSE is deliberately left undefined */
int main(void)
{
/* --- #ifdef: existence only -------------------------------------- */
#ifdef FEATURE_LOGGING
printf("logging is compiled in\n");
#else
/* This branch is DELETED. It could contain anything. */
printf("logging disabled\n");
#endif
/* --- #if: the value matters -------------------------------------- */
#if MAX_CONNECTIONS > 100
printf("high-capacity build\n");
#elif MAX_CONNECTIONS > 10
printf("standard build (%d connections)\n", MAX_CONNECTIONS);
#else
printf("minimal build\n");
#endif
/* --- combining tests, which #ifdef cannot express ---------------- */
#if defined(FEATURE_LOGGING) && !defined(QUIET_MODE)
printf("verbose logging active\n");
#endif
/* --- THE HAZARD: an undefined identifier silently becomes 0 ------ */
#if VERBOSE /* VERBOSE is not defined -> 0 -> false */
printf("this never prints, and nothing warns you\n");
#endif
#if defined(VERBOSE) && VERBOSE /* explicit and safe */
printf("also never prints, but the intent is clear\n");
#endif
/* --- #if 0 to disable a block containing comments ---------------- */
#if 0
printf("dead code\n"); /* a comment in here is harmless */
this does not have to be valid C, only tokenizable;
#endif
return 0;
}
# -Wundef turns the silent-zero hazard into a diagnostic:
$ gcc -Wundef prog.c
prog.c:31:5: warning: "VERBOSE" is not defined, evaluates to 0 [-Wundef]
# See which branch survived -- the definitive way to debug a conditional:
$ gcc -E prog.c | grep -n printf
WHAT THE COMPILER RECEIVES
source after preprocessing
------------------------------ ---------------------------------
#ifdef FEATURE_LOGGING
printf("logging in\n"); -> printf("logging in\n");
#else
printf("disabled\n"); -> (deleted entirely -- never parsed,
#endif never type-checked, never linked)
This is why the dead branch may call functions that do not exist on
this platform: the compiler never sees it.
| Form | Tests | Use for |
|---|---|---|
#ifdef NAME | is NAME defined | feature/platform presence, include guards |
#ifndef NAME | is NAME not defined | include guards, default definitions |
#if expr | integer value of expr | version numbers, sizes, boolean combinations |
#if defined(A) || defined(B) | either defined | multi-platform grouping |
Key Takeaways
- Conditional directives delete code before compilation, so the discarded branch need not be valid for the current platform — something run-time
ifcan never do. - Use
#ifdef/#ifndefwhen only existence matters,#ifwhen a value matters or you need&&/||. - The
#ifevaluator handles integers only: nosizeof, no floats, no enum constants, no string comparison. - Undefined identifiers silently evaluate to
0in#if— build with-Wundefand prefer#if defined(X) && X. - Conditionals multiply build configurations exponentially; isolate platform differences behind a small interface instead of scattering
#ifdef.
🧪 Practice
- Write a file with three mutually exclusive
#if/#elif/#elsebranches keyed on aLEVELmacro, and usegcc -Eto confirm exactly one survives at each-DLEVEL=value.- Misspell a macro name inside an
#ifand observe that the code compiles and takes the wrong branch. Then add-Wundefand fix it.- Put deliberately invalid C inside
#if 0and confirm it compiles; then change it to#if 1and read the errors.- Interview-style: "Why can't you replace
#ifdef _WIN32with a normalifstatement?" Hint: consider what has to be true of both branches for the file to compile at all.
Feature Test Macros
Theory
Standard C is a small language, and its headers are required to declare only what the standard specifies. But <stdio.h> on Linux also has popen, <string.h> has strdup, and <unistd.h> exists at all — none of which are standard C. How does one header serve both a strict-conformance build and a POSIX build?
The answer is feature test macros: macros you define before including any header, which the implementation's headers examine to decide what to declare. They are a request, made at the top of your file or on the command line, for a particular set of extensions.
The mechanism matters more than the list. Inside glibc's headers, roughly:
#ifdef _POSIX_C_SOURCE
extern int fileno(FILE *); /* declared only if POSIX was requested */
#endif
So if you forget the macro, the declaration simply does not exist. In C89 that produced an implicit declaration and a link that often worked by accident; in C99 and later it is an error, which is a considerable improvement.
The macros you will actually meet:
| Macro | Requests |
|---|---|
_POSIX_C_SOURCE 200809L | POSIX.1-2008 (fileno, strdup, getline, nanosleep) |
_XOPEN_SOURCE 700 | X/Open 7 — POSIX plus XSI extensions (strptime, crypt) |
_GNU_SOURCE | everything glibc offers, including GNU-only functions |
_DEFAULT_SOURCE | glibc's default set (BSD and SVID additions) |
__STDC_WANT_LIB_EXT1__ 1 | C11 Annex K bounds-checked functions (memcpy_s) |
Two rules make them work, and violating either produces baffling results:
Define before any #include. The headers test the macro as they are processed; defining it afterwards has no effect on headers already expanded. Because a header you include might itself include <features.h>, the safe place is the very first line of the file — or, better, the compiler command line (-D_POSIX_C_SOURCE=200809L), which guarantees it for every file in the build.
Define it consistently across the whole project. Different translation units seeing different declarations of the same structure is a recipe for silent ABI mismatches.
There is also a converse relationship worth understanding: -std=c11 selects strict conformance and disables the default extensions, whereas -std=gnu11 enables them. So a file that compiles with gcc prog.c may fail with gcc -std=c11 prog.c — the classic "it worked yesterday" symptom, fixed by adding the appropriate feature test macro rather than by abandoning -std=c11.
Note the naming convention: the leading underscore plus capital letter marks these as reserved identifiers, which is precisely why the implementation may use them and why you must never invent your own names in that space.
Examples
/* The feature test macro MUST come before every #include. */
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h> /* POSIX: only usable because of the macro above */
int main(void)
{
/* strdup is POSIX, not standard C: without the macro, this fails to
compile under -std=c11 with "implicit declaration of function". */
char *copy = strdup("duplicated with a POSIX function");
if (!copy) return EXIT_FAILURE;
puts(copy);
free(copy);
/* fileno is POSIX too. */
printf("stdout file descriptor = %d\n", fileno(stdout));
/* getline is POSIX.1-2008: reads an arbitrarily long line, allocating. */
printf("enter a line: ");
char *line = NULL;
size_t cap = 0;
ssize_t len = getline(&line, &cap, stdin);
if (len > 0) printf("read %zd bytes: %s", len, line);
free(line);
/* Report which feature sets the headers were compiled with. */
#ifdef _POSIX_C_SOURCE
printf("_POSIX_C_SOURCE = %ldL\n", (long)_POSIX_C_SOURCE);
#endif
#ifdef _GNU_SOURCE
printf("_GNU_SOURCE is defined\n");
#endif
return 0;
}
# Strict C11: POSIX functions are hidden, so this FAILS without the macro.
$ gcc -std=c11 -Wall prog.c
prog.c: warning: implicit declaration of function 'strdup'
# Two equivalent fixes -- the second is preferred for whole projects:
$ gcc -std=c11 -D_POSIX_C_SOURCE=200809L prog.c # request POSIX
$ gcc -std=gnu11 prog.c # GNU dialect, extensions on
# Prove the mechanism: ask whether the declaration is even present.
$ echo '#include <string.h>' | gcc -std=c11 -E - | grep -c strdup
0
$ echo '#include <string.h>' | gcc -std=c11 -D_POSIX_C_SOURCE=200809L -E - | grep -c strdup
1
HOW A HEADER DECIDES WHAT TO DECLARE
your file glibc <string.h>
--------------------- -----------------------------------
#define _POSIX_C_SOURCE #if defined(_POSIX_C_SOURCE) && \
200809L _POSIX_C_SOURCE >= 200809L
#include <string.h> ---> extern char *strdup(const char *);
#endif
extern void *memcpy(...); /* always */
Define the macro AFTER the include and the #if has already been
evaluated -- the declaration never appears, and nothing warns you.
Key Takeaways
- Feature test macros tell implementation headers which non-standard declarations to expose; without them, the declarations do not exist.
- They must be defined before any
#include— the first line of the file, or better, on the compiler command line for the whole project. _POSIX_C_SOURCE,_XOPEN_SOURCE, and_GNU_SOURCErequest progressively larger sets; define one consistently across all translation units.-std=c11disables extensions while-std=gnu11enables them, which is why the same code can compile under one and not the other.- The leading-underscore-capital naming marks these as reserved to the implementation; never invent identifiers in that space.
🧪 Practice
- Compile a program calling
strdupwith-std=c11alone, then with-D_POSIX_C_SOURCE=200809L, and explain both results.- Move the
#define _POSIX_C_SOURCEline to after#include <string.h>and explain why it stops working.- Use
gcc -Eto prove thatgetline's declaration appears only when the feature test macro is set.- Interview-style: "A colleague's file compiles with
gcc file.cbut fails withgcc -std=c11 file.c. What is happening?" Hint: think about what the default dialect enables that strict conformance does not.
Platform-Specific Code
Theory
Portable C does not mean code that avoids platform differences — it means code that isolates them. Every non-trivial program eventually needs something the standard does not provide: sleeping, listing a directory, querying the terminal size. Conditional compilation is how you supply each platform's version while keeping one source tree.
Detection relies on macros the compiler predefines. They are not standardized, but they are stable and well-documented in practice:
| Category | Macro | Meaning |
|---|---|---|
| OS | _WIN32 | Windows (32- and 64-bit; _WIN64 adds 64-bit) |
__linux__ | Linux | |
__APPLE__ | macOS / iOS (with TargetConditionals.h to distinguish) | |
__unix__ | Unix-like generally | |
__FreeBSD__, __ANDROID__ | as named | |
| Compiler | __GNUC__ | GCC — also defined by Clang and ICC |
__clang__ | Clang specifically; test it before __GNUC__ | |
_MSC_VER | MSVC, with the version as the value | |
| Architecture | __x86_64__, __i386__, __aarch64__, __arm__ | as named |
| Word size | __SIZEOF_POINTER__, UINTPTR_MAX | pointer width |
Two detection traps recur constantly. _WIN32 is defined on 64-bit Windows too, so #ifdef _WIN32 is the correct test for "Windows" and _WIN64 narrows it. And Clang defines __GNUC__ for compatibility, so an #elif defined(__GNUC__) chain must test __clang__ first or Clang will silently take the GCC branch.
The design question is where the conditionals go, and this matters far more than the detection details. Scattering #ifdef through business logic produces code that nobody can read and that only ever compiles on the maintainer's machine. The disciplined pattern is a thin platform layer: define a small portable interface, and implement it once per platform, with the conditionals confined to that one file. The rest of the program calls platform_sleep_ms and never mentions an operating system.
Where a conditional chain must appear inline, always end it with an #else that produces a compile error via #error (see below). Silently compiling nothing on an unrecognized platform turns a five-second build failure into a mysterious run-time bug.
A useful check on your own design: if a header contains #ifdef _WIN32 around a struct definition, different platforms see different layouts of the same type — which is exactly what you want in a platform layer and exactly what causes chaos when it leaks into a public interface.
Examples
/* ---------- platform.h : the portable interface, no #ifdef ---------- */
#ifndef PLATFORM_H
#define PLATFORM_H
#include <stddef.h>
void platform_sleep_ms(unsigned ms);
size_t platform_page_size(void);
const char *platform_name(void);
#endif
/* ---------- platform.c : every conditional lives HERE ---------------- */
#include "platform.h"
#if defined(_WIN32)
# include <windows.h>
#elif defined(__unix__) || defined(__APPLE__)
# include <unistd.h>
# include <time.h>
#else
# error "Unsupported platform: add an implementation in platform.c"
#endif
void platform_sleep_ms(unsigned ms)
{
#if defined(_WIN32)
Sleep(ms); /* milliseconds */
#else
struct timespec ts = { ms / 1000u, (long)(ms % 1000u) * 1000000L };
nanosleep(&ts, NULL); /* seconds + nanoseconds */
#endif
}
size_t platform_page_size(void)
{
#if defined(_WIN32)
SYSTEM_INFO si;
GetSystemInfo(&si);
return (size_t)si.dwPageSize;
#else
return (size_t)sysconf(_SC_PAGESIZE);
#endif
}
const char *platform_name(void)
{
/* Order matters: test the MOST specific macro first. */
#if defined(_WIN64)
return "Windows (64-bit)";
#elif defined(_WIN32)
return "Windows (32-bit)";
#elif defined(__APPLE__)
return "macOS";
#elif defined(__ANDROID__) /* before __linux__: Android defines both */
return "Android";
#elif defined(__linux__)
return "Linux";
#else
return "unknown";
#endif
}
/* ---------- main.c : completely platform-agnostic -------------------- */
#include <stdio.h>
#include "platform.h"
int main(void)
{
printf("platform : %s\n", platform_name());
printf("page size : %zu bytes\n", platform_page_size());
/* Compiler identification: __clang__ MUST be tested before __GNUC__,
because Clang also defines __GNUC__ for compatibility. */
#if defined(__clang__)
printf("compiler : clang\n");
#elif defined(__GNUC__)
printf("compiler : gcc\n");
#elif defined(_MSC_VER)
printf("compiler : msvc\n");
#endif
printf("sleeping 100 ms...\n");
platform_sleep_ms(100);
puts("done");
return 0;
}
TWO WAYS TO ORGANIZE THE SAME DIFFERENCES
SCATTERED (avoid) ISOLATED (prefer)
business.c business.c
#ifdef _WIN32 ... platform_sleep_ms(100);
logic logic
#ifdef _WIN32 ... platform_page_size();
more logic more logic
#ifdef __linux__ ... |
v
Every reader must mentally platform.c <- ALL #ifdef here
run three builds at once. platform.h <- one clean interface
Key Takeaways
- Platform detection uses compiler-predefined macros;
_WIN32covers 64-bit Windows too, and Clang also defines__GNUC__. - Order conditional chains from most specific to least specific:
__clang__before__GNUC__,__ANDROID__before__linux__,_WIN64before_WIN32. - Confine conditionals to a thin platform layer with a portable interface, rather than scattering them through application logic.
- Always terminate a platform chain with
#else #error, so an unsupported target fails at build time rather than at run time. - Code inside a branch you never build will rot — compile every supported configuration in CI.
🧪 Practice
- Write
platform_name()covering Windows, macOS, and Linux, and verify withgcc -Ewhich branch survives on your machine.- Write a chain testing
__GNUC__before__clang__, compile with Clang, and explain the result. Then fix the order.- Implement a two-function platform layer (sleep and page size) with a header containing no conditionals at all, and confirm
main.chas no platform macros.- Interview-style: "How would you structure a codebase supporting three operating systems?" Hint: think about where the conditionals live and how many files a reader must open to understand one function.
Debug and Release Builds
Theory
The same source should produce two rather different programs: a debug build that checks assumptions aggressively and explains itself, and a release build that is fast and quiet. Conditional compilation is what makes both come from one source tree, with the difference driven entirely by macros the build system defines.
The one standardized mechanism is NDEBUG and assert. <assert.h> defines assert(expr) to evaluate expr and abort with a message naming the file, line, and expression if it is false — unless NDEBUG is defined, in which case assert expands to a do-nothing expression. So a release build adds -DNDEBUG and every assertion vanishes, costing nothing.
That design has one consequence you must internalize: never put a side effect inside assert.
assert(pop_item() != NULL); /* the item is never popped in release builds */
The assertion is the entire statement, so when it disappears, so does the call. Assertions must be pure checks.
The complementary rule is about what to assert. Assertions document programmer errors — conditions that must hold if the code is correct: a non-null internal pointer, an index within bounds, an invariant after a rebalance. They are not error handling. A failed file open, a malformed input line, or a failed allocation are run-time conditions that occur in correct programs and must be handled with real code that survives into release.
Beyond assert, projects define their own build-mode macros — conventionally DEBUG, or a project-prefixed name — to gate logging, extra validation, expensive invariant checks, and diagnostic output. The pattern that keeps this maintainable is a debug macro that compiles away to nothing in release but still parses in both modes, so the disabled code cannot rot. do { } while (0) (see 8.3) is the standard shape, and passing arguments to a (void) expression keeps unused-variable warnings quiet.
A typical flag pairing:
| Debug | Release | |
|---|---|---|
| Optimization | -O0 (or -Og) | -O2 / -O3 |
| Debug info | -g3 | -g (kept, then stripped or shipped separately) |
| Assertions | on (no NDEBUG) | off (-DNDEBUG) |
| Sanitizers | -fsanitize=address,undefined | none |
| Hardening | — | -D_FORTIFY_SOURCE=3 -fstack-protector-strong |
Two cautions. First, keep -Wall -Wextra on in both modes: some warnings only appear with optimization enabled, because they depend on data-flow analysis. Second, remember that debug and release are genuinely different programs — a bug that appears only in release usually means undefined behaviour whose symptoms changed, not a compiler bug.
Examples
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
/* A debug-only logging macro. In release it becomes a (void)0 expression
that still TYPE-CHECKS its arguments, so disabled code cannot rot. */
#ifdef DEBUG
# define DBG(fmt, ...) \
fprintf(stderr, "[debug %s:%d] " fmt "\n", __FILE__, __LINE__, ##__VA_ARGS__)
# define DBG_ONLY(stmt) stmt
#else
# define DBG(fmt, ...) ((void)0)
# define DBG_ONLY(stmt) ((void)0)
#endif
/* An expensive invariant check that only exists in debug builds. */
static int is_sorted(const int *a, size_t n)
{
for (size_t i = 1; i < n; i++)
if (a[i - 1] > a[i]) return 0;
return 1;
}
static int binary_search(const int *a, size_t n, int key)
{
/* Programmer errors: these must hold if the caller is correct. */
assert(a != NULL);
assert(is_sorted(a, n)); /* O(n) check, debug builds only */
size_t lo = 0, hi = n;
while (lo < hi) {
size_t mid = lo + (hi - lo) / 2;
DBG("probing index %zu (value %d)", mid, a[mid]);
if (a[mid] == key) return (int)mid;
if (a[mid] < key) lo = mid + 1;
else hi = mid;
}
return -1;
}
int main(int argc, char **argv)
{
int data[] = {1, 3, 5, 7, 9, 11};
/* A RUN-TIME condition: handled properly, survives into release. */
if (argc > 2) {
fprintf(stderr, "usage: %s [key]\n", argv[0]);
return EXIT_FAILURE;
}
int key = (argc == 2) ? atoi(argv[1]) : 7;
DBG_ONLY(printf("searching for %d in %zu elements\n",
key, sizeof data / sizeof *data));
printf("index of %d = %d\n", key, binary_search(data, 6, key));
/* WRONG -- the side effect disappears under -DNDEBUG:
assert(binary_search(data, 6, 3) >= 0); */
/* RIGHT -- compute first, then assert on the pure value. */
int found = binary_search(data, 6, 3);
assert(found >= 0);
printf("found 3 at index %d\n", found);
#ifdef NDEBUG
puts("release build: assertions compiled out");
#else
puts("debug build: assertions active");
#endif
return 0;
}
# Debug build: assertions on, no optimization, sanitizers, full debug info.
$ gcc -std=c11 -Wall -Wextra -g3 -O0 -DDEBUG \
-fsanitize=address,undefined prog.c -o prog-debug
# Release build: optimized, assertions off, hardened.
$ gcc -std=c11 -Wall -Wextra -O2 -DNDEBUG \
-D_FORTIFY_SOURCE=3 -fstack-protector-strong prog.c -o prog-release
$ ./prog-debug 7
[debug prog.c:32] probing index 3 (value 7)
index of 7 = 3
debug build: assertions active
$ ./prog-release 7
index of 7 = 3
release build: assertions compiled out
WHAT assert(x) BECOMES
without NDEBUG: ((x) ? (void)0
: __assert_fail("x", __FILE__, __LINE__, __func__))
with NDEBUG: ((void)0)
Everything inside the parentheses -- including any function call --
is gone. That is the whole reason side effects are forbidden.
Key Takeaways
assertis compiled out by-DNDEBUG, so assertions must be pure checks with no side effects the program depends on.- Assert programmer errors (invariants, contracts); handle run-time conditions such as I/O and allocation failure with real code.
- Write debug macros that expand to
((void)0)in release so the disabled code still type-checks and cannot rot. - Debug builds pair
-O0 -g3with sanitizers; release builds pair-O2 -DNDEBUGwith hardening flags. - Keep
-Wall -Wextraon in both modes — some warnings require optimization to be detected at all.
🧪 Practice
- Write a program with an assertion, build it with and without
-DNDEBUG, and confirm the assertion disappears usinggcc -E.- Put a function call with a side effect inside an
assert, build both ways, and show that the behaviour differs.- Write a
DBGmacro that vanishes in release builds but still triggers a-Wformatwarning in both modes when given a wrong specifier.- Interview-style: "When should you use
assertinstead of returning an error code?" Hint: distinguish conditions caused by a bug from conditions that occur in a correct program.
#error and #pragma
Theory
Two directives step outside the substitute-and-delete model: one deliberately stops the build, and one talks to the compiler.
#error message makes the preprocessor emit a diagnostic and fail. It exists so that an impossible configuration is caught at build time rather than producing a subtly broken binary. Its natural home is the final #else of any conditional chain that must be exhaustive:
#else
# error "Unsupported platform"
#endif
It is equally valuable for enforcing prerequisites: a minimum standard version, a required feature test macro, a compiler that supports an extension you rely on, an assumption about type sizes. The message is free text (no quotes required, though they are conventional) and macros in it are not expanded, so print values with a separate #if chain rather than expecting interpolation. C23 adds #warning for non-fatal notices; GCC and Clang have supported it as an extension for decades.
#pragma is the standard escape hatch for compiler-specific instructions. The standard defines the syntax and almost nothing about the meaning: an unrecognized pragma must be ignored, which is what makes pragmas relatively safe to use — a compiler that does not understand yours simply skips it (though GCC will tell you with -Wunknown-pragmas).
The pragmas that matter in practice:
| Pragma | Effect |
|---|---|
#pragma once | include-guard replacement (universal but non-standard) |
#pragma pack(push, 1) / pop | control struct padding (see Chapter 6.3) |
#pragma GCC diagnostic push/ignored/pop | suppress a warning for a bounded region |
#pragma omp parallel for | OpenMP parallelization |
#pragma STDC FP_CONTRACT OFF | one of three standard pragmas (also FENV_ACCESS, CX_LIMITED_RANGE) |
The diagnostic-control pragmas deserve emphasis because they are the disciplined way to handle a warning you genuinely must accept. Rather than disabling -Wunused-parameter for the whole project, push the diagnostic state, ignore exactly one warning for exactly the lines that need it, and pop. The suppression is then visible, local, and documented at the point of the exception.
_Pragma("...") is the operator form, introduced in C99. Because it takes a string literal, it can be produced by a macro — which #pragma cannot, since directives are not generated by macro expansion. That is the only way to wrap pragmas in a portable macro.
Examples
#include <stdio.h>
#include <limits.h>
/* --- #error as a build-time contract ------------------------------- */
#if !defined(__STDC_VERSION__) || __STDC_VERSION__ < 201112L
# error "This project requires a C11 compiler (use -std=c11 or later)"
#endif
#if CHAR_BIT != 8
# error "This code assumes 8-bit bytes"
#endif
/* Exhaustive chain: an unknown platform fails at build time, loudly. */
#if defined(_WIN32)
# define PLATFORM "windows"
#elif defined(__linux__)
# define PLATFORM "linux"
#elif defined(__APPLE__)
# define PLATFORM "macos"
#else
# error "Unsupported platform -- add a case above"
#endif
/* C23 has #warning; GCC and Clang have supported it for decades. */
#if defined(LEGACY_API)
# warning "LEGACY_API is deprecated and will be removed in v3.0"
#endif
/* --- #pragma for a bounded, documented warning suppression --------- */
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wunused-parameter"
/* This signature is fixed by a callback API we do not control. */
static int callback(int used, void *ignored_by_design)
{
return used * 2;
}
#pragma GCC diagnostic pop /* the suppression ends HERE */
/* --- _Pragma: the operator form, usable inside a macro -------------
#pragma cannot be produced by macro expansion; _Pragma takes a string
literal, so it can. */
#define ALLOW_FLOAT_EQ_BEGIN \
_Pragma("GCC diagnostic push") \
_Pragma("GCC diagnostic ignored \"-Wfloat-equal\"")
#define ALLOW_FLOAT_EQ_END \
_Pragma("GCC diagnostic pop")
int main(void)
{
printf("platform: %s\n", PLATFORM);
printf("callback(21, NULL) = %d\n", callback(21, NULL));
double a = 0.5, b = 0.5;
ALLOW_FLOAT_EQ_BEGIN
int exact = (a == b); /* no -Wfloat-equal inside the region */
ALLOW_FLOAT_EQ_END
/* Outside the region the warning is active again -- compile with
-Wfloat-equal and only the line below is reported. */
int also = (a == b);
printf("exact=%d also=%d, CHAR_BIT=%d\n", exact, also, CHAR_BIT);
return 0;
}
# The #error fires before anything is compiled:
$ gcc -std=c99 prog.c
prog.c:6:4: error: #error "This project requires a C11 compiler (use -std=c11 or later)"
$ gcc -std=c11 prog.c -o prog # succeeds
# Find pragmas your compiler does not recognize:
$ gcc -Wunknown-pragmas -std=c11 prog.c
# Confirm the suppression is bounded: only the line AFTER the pop warns.
$ gcc -std=c11 -Wfloat-equal prog.c
prog.c:57:19: warning: comparing floating-point with '==' or '!=' is unsafe
WHY #error BEATS A SILENT DEFAULT
without #error: with #error:
unknown platform unknown platform
| |
no branch taken build stops immediately
| |
PLATFORM undefined "Unsupported platform --
| add a case above"
confusing error 40 lines
later, or a broken binary Five seconds to diagnose.
Key Takeaways
#errorfails the build with a message; use it in the final#elseof every exhaustive conditional chain and to enforce prerequisites.- Macros are not expanded inside an
#errormessage — select among several messages with#ifinstead. #warningis a long-standing extension standardized in C23 for non-fatal notices such as deprecations.- Unrecognized pragmas must be ignored, which makes them portable to use;
-Wunknown-pragmasreveals which ones your compiler skipped. #pragma GCC diagnostic push/ignored/popscopes a warning suppression to a few lines;_Pragma("...")is the form usable inside macros.
🧪 Practice
- Add an
#errorrequiring C11 and build with-std=c99and-std=c11. Compare the diagnostics.- Write a platform chain whose
#elseis an#error, then compile with-U__linux__ -U_WIN32 -U__APPLE__to trigger it.- Use
#pragma GCC diagnostic push/ignored/popto silence one warning for exactly one function, and confirm the warning returns immediately after thepop.- Interview-style: "Why does the standard require unknown pragmas to be ignored rather than rejected?" Hint: think about compiling code written for a different compiler, and what the alternative would cost.
<a id="83-macro-techniques-and-hazards"></a>
8.3 Macro Techniques and Hazards
This section covers what macros can do that nothing else in C can — turn tokens into strings, build identifiers, and generate code — alongside the traps that make them notorious, and the modern features that let you retire many of them.
Stringification and Token Pasting
Theory
Two operators exist only inside function-like macros, and they are what make macros a code-generation tool rather than merely a substitution one.
# — stringification. Applied to a macro parameter, #param produces a string literal containing the argument's source text. STR(x + y) yields "x + y". The preprocessor normalizes whitespace to single spaces and escapes embedded quotes and backslashes correctly. This is how assert prints the failing expression, and how a test framework reports which comparison failed.
## — token pasting. a ## b glues two tokens into one. CONCAT(my, var) yields the single identifier myvar. This is how you generate names: make_##type##_list becomes make_int_list. The result must be a valid single token; pasting 1 and + is undefined behaviour.
Both operators come with one rule that surprises everyone the first time: a parameter operated on by # or ## is not macro-expanded first. So:
#define VERSION 3
#define STR(x) #x
STR(VERSION) /* "VERSION", not "3" */
The fix is the two-level idiom: an outer macro whose only job is to expand its argument, and an inner macro that does the stringification.
#define STR_IMPL(x) #x
#define STR(x) STR_IMPL(x) /* x expands, THEN gets stringified */
STR(VERSION) /* "3" */
The same two-level pattern applies to ##. The rule to remember: if the argument might itself be a macro, add an indirection layer. Naming the inner one with an _IMPL or _ suffix is the near-universal convention.
Where these earn their keep:
- Diagnostics — printing the expression that failed, which no function can do.
- Identifier generation — declaring or defining families of similarly named functions, types, or variables from one line.
- Unique names —
__LINE__pasted into an identifier gives a per-line unique symbol, the trick behind static assertions and scoped-cleanup macros in C. - Turning a build-system value into a string —
-DVERSION=1.2.3becomes"1.2.3"via the two-level idiom.
Examples
#include <stdio.h>
#include <string.h>
/* --- stringification --------------------------------------------- */
#define STR_IMPL(x) #x
#define STR(x) STR_IMPL(x) /* expands x before stringifying */
/* --- token pasting ------------------------------------------------ */
#define CAT_IMPL(a, b) a ## b
#define CAT(a, b) CAT_IMPL(a, b) /* expands both before pasting */
#define VERSION_MAJOR 3
#define VERSION_MINOR 14
/* Generating a family of functions from one line -- the payoff. */
#define DEFINE_MAX(type) \
static type max_##type(type a, type b) \
{ \
return a > b ? a : b; \
}
DEFINE_MAX(int) /* defines max_int */
DEFINE_MAX(long) /* defines max_long */
DEFINE_MAX(double) /* defines max_double */
/* A check macro that reports the source text of what failed. */
#define CHECK(expr) \
do { \
if (!(expr)) \
printf("FAILED: %s (%s:%d)\n", #expr, \
__FILE__, __LINE__); \
else \
printf("passed: %s\n", #expr); \
} while (0)
/* __LINE__ pasted into a name gives a unique identifier per line. */
#define UNIQUE_NAME(prefix) CAT(prefix, __LINE__)
int main(void)
{
/* The one-level trap and the two-level fix, side by side. */
printf("STR_IMPL(VERSION_MAJOR) = %s <- not expanded\n",
STR_IMPL(VERSION_MAJOR)); /* "VERSION_MAJOR" */
printf("STR(VERSION_MAJOR) = %s <- expanded first\n",
STR(VERSION_MAJOR)); /* "3" */
/* Building a version string entirely at compile time. */
printf("version = %s\n",
STR(VERSION_MAJOR) "." STR(VERSION_MINOR)); /* "3" "." "14" */
/* Whitespace is normalized; quotes and backslashes are escaped. */
printf("STR(a + b) = \"%s\"\n", STR(a + b));
printf("STR(\"quoted\") = %s\n", STR("quoted"));
/* Generated functions, called normally. */
printf("max_int(3, 7) = %d\n", max_int(3, 7));
printf("max_long(10L, 4L) = %ld\n", max_long(10L, 4L));
printf("max_double(2.5, 1.5) = %.1f\n", max_double(2.5, 1.5));
CHECK(1 + 1 == 2);
CHECK(strlen("abc") == 5);
/* Both halves must sit on the SAME source line to paste the same
number -- that is exactly what makes the name unique per line. */
int UNIQUE_NAME(tmp_) = 1; (void)UNIQUE_NAME(tmp_); /* tmp_71, say */
return 0;
}
EXPANSION OF THE TWO-LEVEL IDIOM
STR(VERSION_MAJOR)
-> STR_IMPL(VERSION_MAJOR) outer macro: argument IS expanded
because # is not applied here
-> STR_IMPL(3) VERSION_MAJOR expanded to 3
-> #3
-> "3" correct
STR_IMPL(VERSION_MAJOR) called directly:
-> #VERSION_MAJOR # suppresses expansion
-> "VERSION_MAJOR" surprising
| Operator | Input | Output | Common use |
|---|---|---|---|
#x | x = a + b | "a + b" | assertion and test messages |
## | foo ## bar | foobar | generating function and type names |
two-level # | macro-valued argument | expanded, then quoted | -DVER=1.2 to "1.2" |
## with __LINE__ | tmp ## 71 | tmp71 | unique per-line identifiers |
Key Takeaways
#paramstringifies a parameter's source text;##pastes two tokens into one, which must be a valid single token.- A parameter used with
#or##is not macro-expanded first — this is the rule behind nearly every stringification surprise. - Use the two-level
_IMPLidiom whenever the argument might itself be a macro. - Token pasting generates families of identifiers, which is the foundation of X-Macros and of generic containers in C.
- Pasting
__LINE__into a name yields per-line unique identifiers, the basis of static assertion and cleanup macros.
🧪 Practice
- Write
STR(x)in one level and two, apply both to a macro-valued argument, and explain the difference in output.- Write
DEFINE_SWAP(type)that generatesswap_int,swap_double, andswap_char, and call all three.- Use
-DBUILD_ID=abc123and print it as a string with the two-level idiom. Verify withgcc -Ethat the expansion is what you expect.- Interview-style: "Why does
#define STR(x) #xprintVERSIONinstead of3forSTR(VERSION)?" Hint: name the rule about parameters adjacent to#and##.
Multiple Evaluation Pitfalls
Theory
This is the single most notorious macro bug, and it follows inevitably from text substitution.
A macro parameter is pasted wherever it appears in the replacement text. If it appears twice, the argument's expression appears twice — and if that expression has a side effect, the side effect happens twice.
#define MAX(a, b) ((a) > (b) ? (a) : (b))
int i = 5, j = 3;
int m = MAX(i++, j);
/* expands to: ((i++) > (j) ? (i++) : (j))
i is incremented TWICE, and m gets 6, not 5 */
The parentheses are all correct. The macro is textbook-quality. It is still wrong, because the caller passed an expression with a side effect, and nothing in the macro's appearance warns them.
Side effects hide in more places than ++:
- assignment and compound assignment (
x = f(),n += 1) - any function call that is not pure —
getchar(),rand(),pop(),malloc() - volatile reads, where each access is a distinct hardware operation
- expensive-but-pure calls, where double evaluation is a correctness non-issue but a performance one:
MAX(expensive(x), expensive(y))runs three calls instead of two
The consequences split into three severities: wrong results (the i++ case), performance loss (repeated expensive calls), and hardware misbehaviour (a volatile register read consumed twice).
The defences, in order of preference:
- Use a
static inlinefunction. Arguments are evaluated exactly once, by the language's own rules. This solves the problem completely and is the right answer whenever the operation is a computation on values. - Assign arguments to temporaries inside the macro. This requires a statement-expression, since a plain macro cannot declare variables and still be an expression. GCC and Clang provide
({ ... }); it is not standard C. - Use
_Generic(C11) to dispatch to type-specific inline functions, getting one evaluation and polymorphism. - Name the macro so callers are warned. If a macro genuinely must evaluate twice, the convention is a name that shouts — some codebases require such macros to be prefixed or documented at the definition.
A related hazard is worth flagging: a macro that evaluates a parameter zero times. #define DEBUG_ONLY(x) (expanding to nothing) will silently delete DEBUG_ONLY(must_happen()) in release builds — the same class of bug as assert with side effects.
Examples
#include <stdio.h>
#define MAX_MACRO(a, b) ((a) > (b) ? (a) : (b))
/* The correct answer whenever the operation is a computation. */
static inline int max_int(int a, int b) { return a > b ? a : b; }
/* GNU statement expression: one evaluation, still type-generic.
NOT standard C -- gcc/clang only. */
#if defined(__GNUC__)
# define MAX_SAFE(a, b) \
({ __auto_type _a = (a); \
__auto_type _b = (b); \
_a > _b ? _a : _b; })
#endif
static int call_count = 0;
static int expensive(int x) { call_count++; return x * 2; }
int main(void)
{
/* --- side effect evaluated twice ------------------------------ */
int i = 5, j = 3;
int m = MAX_MACRO(i++, j); /* ((i++) > (j) ? (i++) : (j)) */
printf("MAX_MACRO(i++, j): m=%d i=%d <- i incremented twice\n", m, i);
i = 5;
int n = max_int(i++, j); /* i++ evaluated exactly once */
printf("max_int(i++, j) : n=%d i=%d <- correct\n", n, i);
/* --- performance: three calls where two were intended --------- */
call_count = 0;
int r1 = MAX_MACRO(expensive(3), expensive(4));
printf("MAX_MACRO: result=%d, expensive() called %d times\n",
r1, call_count);
call_count = 0;
int r2 = max_int(expensive(3), expensive(4));
printf("max_int : result=%d, expensive() called %d times\n",
r2, call_count);
#if defined(__GNUC__)
call_count = 0;
int r3 = MAX_SAFE(expensive(3), expensive(4));
printf("MAX_SAFE : result=%d, expensive() called %d times\n",
r3, call_count);
i = 5;
printf("MAX_SAFE(i++, j) = %d, i = %d\n", MAX_SAFE(i++, j), i);
#endif
return 0;
}
Output:
MAX_MACRO(i++, j): m=6 i=7 <- i incremented twice
max_int(i++, j) : n=5 i=6 <- correct
MAX_MACRO: result=8, expensive() called 3 times
max_int : result=8, expensive() called 2 times
MAX_SAFE : result=8, expensive() called 2 times
MAX_SAFE(i++, j) = 5, i = 6
WHY THE COUNT IS THREE
MAX_MACRO(expensive(3), expensive(4))
-> ((expensive(3)) > (expensive(4)) ? (expensive(3)) : (expensive(4)))
^ call 1 ^ call 2 ^ call 3 (the winning branch)
The comparison evaluates both; the selected branch evaluates one again.
The ternary's short-circuit means only ONE of the last two runs -- so
three calls total, not four.
| Approach | Single evaluation | Type-generic | Standard C | Constant expression |
|---|---|---|---|---|
| Plain macro | no | yes | yes | yes |
static inline function | yes | no | yes | no |
| GNU statement expression | yes | yes | no | no |
_Generic + inline functions | yes | yes | yes (C11) | no |
Key Takeaways
- A macro parameter used twice evaluates its argument twice — the classic source of wrong results, wasted work, and volatile-access bugs.
- Side effects hide in
++, assignments, impure function calls, and volatile reads, not just obvious increments. - Prefer a
static inlinefunction whenever the macro is really a computation on values; the language then guarantees one evaluation. - GNU statement expressions with temporaries fix the problem generically but are non-standard;
_Genericplus inline functions is the portable equivalent. - A macro that evaluates a parameter zero times is equally dangerous — it silently deletes the caller's side effect.
🧪 Practice
- Write
MAX(a, b)as a macro, call it withMAX(i++, j)fori = 5, j = 3, and predictmandibefore running.- Instrument a function with a call counter and show that
MAX(f(1), f(2))calls it three times while an inline version calls it twice.- Rewrite
MAXas a GNU statement expression with temporaries, and confirm side effects occur once.- Interview-style: "Why does
MAX(x++, y++)behave unpredictably?" Hint: write out the full expansion, then count each variable's occurrences and consider the ternary's short-circuit.
do-while(0) Idiom
Theory
A multi-statement macro has a syntax problem that has nothing to do with what it computes.
Consider a macro meant to be used like a statement:
#define SWAP(a, b) int t = a; a = b; b = t;
Two things break immediately. First, in if (x) SWAP(p, q); else ..., only the first statement belongs to the if; the rest execute unconditionally and the else becomes an orphan — a syntax error at best, and silently wrong control flow at worst. Second, the macro declares t in the surrounding scope, colliding with any existing t.
The obvious fix — wrapping in braces — solves the scope problem and half the control-flow one:
#define SWAP(a, b) { int t = a; a = b; b = t; }
but now if (x) SWAP(p, q); else ... expands to if (x) { ... } ; else ..., and that stray semicolon between the block and else is a syntax error. The macro works without a trailing semicolon and breaks with one, which means it does not look or behave like a statement.
The idiom that solves both:
#define SWAP(a, b) do { int t = (a); (a) = (b); (b) = t; } while (0)
do { ... } while (0) executes its body exactly once, and — this is the point — it is a statement that requires a terminating semicolon. So SWAP(p, q); is exactly one grammatical statement, works unbraced in if/else, and gives the macro its own scope for temporaries. Any decent optimizer removes the loop entirely; there is no run-time cost.
Two refinements complete the pattern. A trailing semicolon is now mandatory, which is what you want: forgetting it is a compile error rather than a subtle bug. And break or continue inside the body will bind to the do-loop rather than to an enclosing loop, so a macro body must not use them for the caller's control flow — one of the reasons a "return the value" macro is better written as a statement expression or a function.
You will see the same shape in the Linux kernel, in glibc, and in essentially every C project of size. When you need a macro that acts like a statement, this is the shape.
Examples
#include <stdio.h>
/* --- three versions, only one of which is safe -------------------- */
#define SWAP_BAD(a, b) int t = a; a = b; b = t;
#define SWAP_BRACES(a, b) { int t = (a); (a) = (b); (b) = t; }
#define SWAP(a, b) do { int t = (a); (a) = (b); (b) = t; } while (0)
/* The idiom shines for multi-statement macros with a condition. */
#define LOG_IF(cond, msg) \
do { \
if (cond) { \
printf("[log] %s\n", (msg)); \
fflush(stdout); \
} \
} while (0)
/* A cleanup macro: several statements, used like one. */
#define FREE_AND_NULL(p) \
do { free(p); (p) = NULL; } while (0)
int main(void)
{
int x = 1, y = 2;
/* SWAP_BAD in an if/else does NOT compile:
if (x < y) SWAP_BAD(x, y); else puts("no");
expands to:
if (x < y) int t = x; x = y; y = t; ; else puts("no");
^ orphan else */
/* SWAP_BRACES breaks with the semicolon a reader will always write:
if (x < y) SWAP_BRACES(x, y); else puts("no");
expands to:
if (x < y) { ... } ; else puts("no");
^ stray semicolon before else */
/* SWAP works exactly like a statement, braces or not. */
if (x < y)
SWAP(x, y); /* one statement, semicolon required */
else
puts("already ordered");
printf("x=%d y=%d\n", x, y);
/* It also composes inside loops and other conditionals. */
for (int i = 0; i < 3; i++)
LOG_IF(i % 2 == 0, "even iteration");
/* The temporary 't' is scoped to the macro: no collision. */
int t = 99; /* would clash with SWAP_BAD's 't' */
SWAP(x, y);
printf("outer t is untouched: %d, x=%d y=%d\n", t, x, y);
return 0;
}
EXPANSION IN AN if/else, SIDE BY SIDE
source: if (c) SWAP(x, y); else f();
BAD: if (c) int t = x; x = y; y = t; ; else f();
^ only this belongs to the if ^ orphan else
BRACES: if (c) { int t = x; x = y; y = t; } ; else f();
^ stray ; -> error
do/while: if (c) do { ... } while (0); else f();
^------- one complete statement -------^ correct
| Wrapper | Works in unbraced if/else | Needs trailing ; | Own scope |
|---|---|---|---|
| bare statements | no | inconsistent | no |
{ ... } | no (stray semicolon) | must be omitted | yes |
do { ... } while (0) | yes | yes, required | yes |
Key Takeaways
- A multi-statement macro must expand to exactly one statement, or it breaks unbraced
if/elseand silently changes control flow. - Plain braces fix scoping but leave a stray semicolon before
else;do { } while (0)fixes both. - The idiom makes the trailing semicolon mandatory, so the macro reads and behaves like a normal statement call.
- It gives the macro body its own scope for temporaries, avoiding collisions with caller variables.
- Optimizers remove the loop entirely, so there is no run-time cost; avoid
break/continuein the body, which would bind to thedo.
🧪 Practice
- Write
SWAPwithout any wrapper, use it in an unbracedif/else, and read the compiler error. Then fix it withdo { } while (0).- Write a version wrapped in plain braces and show that it compiles only when the caller omits the semicolon.
- Write a
CHECK_AND_RETURN(cond, code)macro using the idiom, and explain whybreakinside its body would be a bug.- Interview-style: "Why
do { } while (0)rather than just braces?" Hint: write out both expansions in anif/elseand look at the semicolon.
X-Macros
Theory
Programs constantly need the same list of items expressed in several forms: an enum of error codes, an array of their names, a switch mapping each to a message, a parser recognizing each by string. Maintaining four parallel lists is a guaranteed bug — someone adds an entry to three of them.
X-Macros solve this by writing the list exactly once and generating every form from it. The technique has two parts:
- A list macro that invokes an undefined macro
Xonce per item, passing the item's fields as arguments. - For each output form, define
Xto produce that form, expand the list, then#undef X.
#define COLOR_LIST \
X(RED, "red", 0xFF0000) \
X(GREEN, "green", 0x00FF00) \
X(BLUE, "blue", 0x0000FF)
/* Form 1: an enum */
#define X(name, str, hex) COLOR_##name,
enum color { COLOR_LIST COLOR_COUNT };
#undef X
/* Form 2: a name table */
#define X(name, str, hex) str,
static const char *color_names[] = { COLOR_LIST };
#undef X
Add a fourth colour to COLOR_LIST and the enum, the table, and every other generated form update together. It is impossible for them to drift out of sync, because there is only one list.
The pattern is decades old (the X name comes from early Unix source) and is used in the Linux kernel, in interpreters for opcode tables, in embedded register maps, and in protocol implementations. It is the closest thing C has to compile-time reflection.
Two practical rules keep it manageable. Always #undef X after each use, or the next expansion inherits the wrong definition. And keep the field list stable: every consumer must accept the same arity, so adding a field means touching every #define X — which is exactly the moment to consider whether the list has outgrown the technique.
The honest trade-off: X-Macros produce code no debugger will show you and no grep will find. Searching for COLOR_RED finds only the list entry, not the enum constant it generates. That opacity is the price for the guarantee that the forms cannot disagree, and it is why the technique belongs in the small, stable, list-shaped corners of a program rather than everywhere.
Examples
#include <stdio.h>
#include <string.h>
/* ================= THE SINGLE SOURCE OF TRUTH ==================== */
/* enum name string HTTP code retryable */
#define ERROR_LIST \
X(OK, "ok", 200, 0) \
X(NOT_FOUND, "not found", 404, 0) \
X(TIMEOUT, "timeout", 408, 1) \
X(RATE_LIMIT, "rate limited", 429, 1) \
X(SERVER, "server error", 500, 1)
/* ---- Form 1: the enum ------------------------------------------- */
#define X(name, str, code, retry) ERR_##name,
typedef enum { ERROR_LIST ERR_COUNT } ErrorCode;
#undef X
/* ---- Form 2: a name lookup table -------------------------------- */
#define X(name, str, code, retry) str,
static const char *error_names[] = { ERROR_LIST };
#undef X
/* ---- Form 3: a numeric lookup table ----------------------------- */
#define X(name, str, code, retry) code,
static const int error_http[] = { ERROR_LIST };
#undef X
/* ---- Form 4: a switch-based predicate --------------------------- */
static int error_is_retryable(ErrorCode e)
{
switch (e) {
#define X(name, str, code, retry) case ERR_##name: return retry;
ERROR_LIST
#undef X
default: return 0;
}
}
/* ---- Form 5: a reverse parser ----------------------------------- */
static ErrorCode error_from_string(const char *s)
{
#define X(name, str, code, retry) if (strcmp(s, str) == 0) return ERR_##name;
ERROR_LIST
#undef X
return ERR_COUNT; /* not found */
}
/* ---- Form 6: generated declarations ------------------------------ */
#define X(name, str, code, retry) void handle_##name(void);
ERROR_LIST /* void handle_OK(void); etc. */
#undef X
int main(void)
{
printf("%-14s %-16s %-6s %s\n", "ENUM", "NAME", "HTTP", "RETRY");
for (int e = 0; e < ERR_COUNT; e++)
printf("%-14d %-16s %-6d %s\n",
e, error_names[e], error_http[e],
error_is_retryable((ErrorCode)e) ? "yes" : "no");
printf("\nlookup \"timeout\" -> enum %d (%s)\n",
error_from_string("timeout"),
error_names[error_from_string("timeout")]);
/* Adding one line to ERROR_LIST updates ALL six forms at once. */
printf("total error codes: %d\n", ERR_COUNT);
return 0;
}
Output:
ENUM NAME HTTP RETRY
0 ok 200 no
1 not found 404 no
2 timeout 408 yes
3 rate limited 429 yes
4 server error 500 yes
lookup "timeout" -> enum 2 (timeout)
total error codes: 5
ONE LIST, MANY FORMS
ERROR_LIST
|
+--------+-------+-------+--------+---------+
v v v v v v
enum names[] http[] switch parser declarations
Add X(CONFLICT, "conflict", 409, 0) to the list and every branch
above regenerates. Nothing can fall out of sync, because nothing
else states the list.
Key Takeaways
- X-Macros declare a list once and generate every derived form from it, making parallel tables impossible to desynchronize.
- The pattern is: a list macro invoking
X(...)per item, then#define X, expand the list,#undef Xfor each output form. - Always
#undef Xafter each use, or later expansions inherit the wrong definition. - They are C's nearest equivalent to compile-time reflection, used for opcode tables, error codes, register maps, and protocol fields.
- The cost is discoverability: generated identifiers are invisible to
grepand to the debugger, so keep the technique to small, stable lists.
🧪 Practice
- Build an X-Macro list of five HTTP status codes and generate an enum plus a name table; add a sixth entry and confirm both update.
- Add a
switch-basedis_error()predicate over the same list without touching the list itself.- Use
gcc -Eto see the generated enum and table, and explain each line of the expansion.- Interview-style: "How would you keep an enum and its string names from drifting apart?" Hint: think about how many places the list of items is written down.
Generic Programming with _Generic
Theory
Before C11, writing one operation that worked for several types meant one of three unhappy options: a macro with no type checking, a family of hand-written _int/_double functions the caller had to choose between, or void * plus a size and a comparison callback (the qsort approach), which loses type safety entirely.
C11 added _Generic, a type-based selection expression. It examines the type of a controlling expression at compile time and evaluates to whichever association matches:
_Generic(expression,
int: handler_int,
double: handler_double,
default: handler_other)
Three properties are essential:
- Selection happens at compile time. The result is a single expression with a single type; the unselected branches are discarded. There is no run-time dispatch and no cost.
- The controlling expression is not evaluated. Only its type is used, so
_Generic(i++, ...)does not incrementi. This makes it side-effect safe in a way that macros generally are not. - The type must match exactly after the usual conversions. Array and function types decay to pointers, and top-level qualifiers are dropped from the controlling expression, but
intwill not match alongassociation. Every type you want to support must be listed, or you need adefault.
The dominant use is as the dispatch mechanism inside a macro, giving a type-generic interface backed by ordinary type-safe functions. That combination solves the multiple-evaluation problem too: the macro's argument is passed to a real function, so it is evaluated exactly once.
The standard library uses this directly: <tgmath.h> builds type-generic sqrt, sin, and friends over the float/double/long double variants, and it is how a C11 implementation provides them portably.
Practical limitations to know going in. There is no way to write a generic association such as "any pointer type" — every type is listed explicitly, so a _Generic over many types gets verbose. Every listed branch must compile even though only one is selected, so you cannot use it to guard code that would be invalid for other types. And it dispatches on type, not on value or on number of arguments.
Examples
#include <stdio.h>
#include <math.h>
#include <string.h>
/* --- type-safe implementations, one per type ---------------------- */
static int max_i(int a, int b) { return a > b ? a : b; }
static long max_l(long a, long b) { return a > b ? a : b; }
static double max_d(double a, double b) { return a > b ? a : b; }
/* --- one generic interface over them ------------------------------
Arguments go to a real function, so each is evaluated exactly once. */
#define MAX(a, b) _Generic((a), \
int: max_i, \
long: max_l, \
double: max_d \
)((a), (b))
/* --- a type-name utility: selection with no function call --------- */
#define TYPE_NAME(x) _Generic((x), \
char: "char", \
signed char: "signed char", \
unsigned char: "unsigned char", \
short: "short", \
int: "int", \
long: "long", \
long long: "long long", \
float: "float", \
double: "double", \
char *: "char *", \
const char *: "const char *", \
void *: "void *", \
default: "unknown")
/* --- a generic print ---------------------------------------------
Dispatch on the FORMAT STRING, not on a whole printf call. Every
association is type-checked even though only one is selected, so
_Generic((x), int: printf("%d\n", (x)), long: printf("%ld\n", (x)), ...)
emits -Wformat warnings for the branches that were not chosen. */
#define FMT(x) _Generic((x), \
int: "%d\n", \
long: "%ld\n", \
double: "%f\n", \
char *: "%s\n")
#define PRINT(x) printf(FMT(x), (x))
static int side_effect_count = 0;
static int bump(void) { side_effect_count++; return 7; }
int main(void)
{
printf("MAX(3, 7) = %d\n", MAX(3, 7));
printf("MAX(2.5, 1.5) = %.1f\n", MAX(2.5, 1.5));
printf("MAX(10L, 20L) = %ld\n", MAX(10L, 20L));
printf("\ntype of 42 : %s\n", TYPE_NAME(42));
printf("type of 42L : %s\n", TYPE_NAME(42L));
printf("type of 3.14 : %s\n", TYPE_NAME(3.14));
printf("type of 3.14f : %s\n", TYPE_NAME(3.14f));
printf("type of \"text\" : %s\n", TYPE_NAME("text")); /* char * */
/* The controlling expression is NOT evaluated -- only its type. */
side_effect_count = 0;
const char *t = TYPE_NAME(bump());
printf("\nTYPE_NAME(bump()) = %s, bump() called %d times\n",
t, side_effect_count); /* 0 times */
/* And because MAX forwards to a function, each argument runs once. */
side_effect_count = 0;
int mv = MAX(bump(), 3); /* compute first: argument order is unspecified */
printf("MAX(bump(), 3) = %d, bump() called %d time\n",
mv, side_effect_count); /* 1 time */
PRINT(42);
PRINT(2.718);
PRINT("generic printing");
return 0;
}
COMPILE-TIME SELECTION
MAX(2.5, 1.5)
-> _Generic((2.5), int: max_i, long: max_l, double: max_d)((2.5), (1.5))
^^^ type is double
-> max_d((2.5), (1.5)) the other branches are DISCARDED
The generated code contains one direct call to max_d. There is no
dispatch table, no branch, and no run-time cost whatsoever.
| Approach | Type-safe | One evaluation | Zero run-time cost | Standard |
|---|---|---|---|---|
| Plain macro | no | no | yes | yes |
void * + callback | no | yes | no (indirect call) | yes |
| Per-type functions | yes | yes | yes | yes (verbose to call) |
_Generic + functions | yes | yes | yes | C11 |
Key Takeaways
_Genericselects an expression based on the compile-time type of its controlling expression, with no run-time cost.- The controlling expression is never evaluated — only its type is inspected — so it is free of side effects.
- Types must match exactly after array/function decay and qualifier removal; list every type you support or provide a
default. - Every association is type-checked even though only one is selected, so dispatch on a value (a format string, a function name) rather than wrapping whole calls.
- The idiomatic use is a macro that dispatches to per-type
static inlinefunctions: type-generic interface, type-safe implementation, single evaluation. <tgmath.h>is built on exactly this mechanism; the limitation is verbosity, since there is no way to match a category of types.
🧪 Practice
- Write
TYPE_NAME(x)covering six types and test it with literals of each, explaining the result for"text"and for'a'.- Build a generic
ABS(x)dispatching toabs,labs, andfabs, and confirm the correct one is chosen for each type.- Prove that
_Genericdoes not evaluate its controlling expression by passing a function with a side-effect counter.- Interview-style: "How does
_Genericdiffer from C++ templates?" Hint: consider what each one generates, when the selection happens, and whether new code is produced per type.
Macros vs inline Functions
Theory
Historically, macros were the only way to avoid function call overhead in C. That reason is gone: inline arrived in C99, and modern compilers inline aggressively across a translation unit and — with link-time optimization — across the whole program, often better than a human would choose. Performance is no longer an argument for macros.
What remains is a genuine capability difference. A macro operates on tokens before compilation; a function operates on values after it. Each can do things the other cannot, and the boundary is sharp:
Only a macro can:
- take a type as an argument (
sizeof (T), casts, declarations) - see the caller's
__FILE__,__LINE__, and source text (#expr) - generate declarations, definitions, or identifiers (X-Macros,
##) - produce an integer constant expression usable as an array size or
caselabel - work in a pre-C99 codebase where
inlinedoes not exist
Only a function can:
- type-check its arguments and its return value
- guarantee each argument is evaluated exactly once
- be stepped into by a debugger, appear in a stack trace, and be profiled by name
- have its address taken and be passed as a callback
- obey scope, so a local name cannot silently capture a caller's identifier
A few notes on inline itself, since its C semantics are unusual. inline is a hint about linkage and duplication, not a command to inline — the compiler decides regardless, and -O0 typically inlines nothing. A bare inline function in C provides only an "inline definition", which is not an external definition, so calling it from another translation unit gives a link error; static inline in a header is the form you want, and it is what real projects use almost exclusively.
The practical decision procedure is short. Ask: does this need tokens, types, or the call site? If yes, write a macro, name it in SHOUTING_CASE, parenthesize everything, and use do { } while (0) if it is statement-shaped. If no, write a static inline function. If you want a generic interface with type safety, combine both: a macro that uses _Generic to dispatch to inline functions.
Examples
#include <stdio.h>
#include <stdlib.h>
/* ============ CASES WHERE ONLY A MACRO WORKS ==================== */
/* 1. Operates on a TYPE, and yields a constant expression. */
#define ARRAY_LEN(a) (sizeof (a) / sizeof (a)[0])
#define ALLOC_ARRAY(T, n) ((T *)malloc((n) * sizeof (T)))
/* 2. Captures the call site. A function could never know these. */
#define TRACE(msg) \
printf("%s:%d in %s(): %s\n", __FILE__, __LINE__, __func__, (msg))
/* 3. Reports the source text of an expression. */
#define REQUIRE(expr) \
do { \
if (!(expr)) { \
fprintf(stderr, "requirement failed: %s at %s:%d\n", \
#expr, __FILE__, __LINE__); \
abort(); \
} \
} while (0)
/* 4. A compile-time constant usable where only constants are allowed. */
#define CACHE_LINE 64
static char aligned_buffer[CACHE_LINE * 8]; /* array size: needs a macro */
/* ============ CASES WHERE A FUNCTION IS BETTER ================== */
/* Type-checked, single evaluation, debuggable, address-takeable. */
static inline int clamp_i(int v, int lo, int hi)
{
return v < lo ? lo : (v > hi ? hi : v);
}
static inline double clamp_d(double v, double lo, double hi)
{
return v < lo ? lo : (v > hi ? hi : v);
}
/* ============ THE BEST OF BOTH: _Generic over inline functions === */
#define CLAMP(v, lo, hi) _Generic((v), \
int: clamp_i, \
double: clamp_d \
)((v), (lo), (hi))
int main(void)
{
int nums[] = {1, 2, 3, 4, 5, 6};
printf("ARRAY_LEN(nums) = %zu (a function receives a pointer)\n",
ARRAY_LEN(nums));
int *heap = ALLOC_ARRAY(int, 10); /* a type as an argument */
if (heap) { heap[0] = 1; free(heap); }
TRACE("only a macro sees this location");
printf("CLAMP(15, 0, 10) = %d\n", CLAMP(15, 0, 10));
printf("CLAMP(0.5, 1.0, 2.0) = %.1f\n", CLAMP(0.5, 1.0, 2.0));
/* A function has an address; a macro does not exist at run time. */
int (*fn)(int, int, int) = clamp_i;
printf("through a function pointer: %d\n", fn(99, 0, 10));
REQUIRE(ARRAY_LEN(nums) == 6);
printf("buffer size = %zu\n", sizeof aligned_buffer);
return 0;
}
THE DECISION
Does it need TOKENS, TYPES, or the CALL SITE?
|
+-- yes --> MACRO
| - SHOUTING_CASE name
| - parenthesize every parameter and the whole body
| - do { } while (0) if it is statement-shaped
| - #undef it if it is a local helper
|
+-- no ---> static inline FUNCTION
- type-checked, one evaluation, debuggable
- put it in the header; the compiler inlines it
Need a generic INTERFACE with type safety?
--> a macro using _Generic that dispatches to inline functions
| Property | Macro | static inline function |
|---|---|---|
| Argument type checking | none | full |
| Evaluations per argument | one per textual use | exactly one |
| Accepts a type argument | yes | no |
Sees __FILE__ / __LINE__ / #expr | yes | no |
| Yields a constant expression | yes | no |
| Debugger and profiler support | none | full |
| Address can be taken | no | yes |
| Respects scope | no | yes |
| Run-time cost when optimized | none | none |
Key Takeaways
- Performance is no longer a reason to choose macros — compilers inline
static inlinefunctions as well as or better than hand-written macros. - Use a macro only when you need tokens, types, the call site, or a constant expression; otherwise use a function.
static inlinein a header is the correct form in C; a bareinlinedefinition is not an external definition and will not link.- Functions give type checking, single evaluation, scoping, addressability, and debugger support — everything a macro gives up.
- A macro dispatching through
_Genericto inline functions combines a generic interface with full type safety and one evaluation.
🧪 Practice
- Write
SQUAREas both a macro and astatic inlinefunction, compile at-O2, and compare the generated assembly withgcc -S.- Write
ARRAY_LENas a function and explain why it cannot work. Then explain why the macro is also wrong when handed a pointer.- Take the address of an inline function and pass it as a callback; then try the same with a macro and describe the error.
- Interview-style: "Given modern compilers inline aggressively, why do macros still exist in new C code?" Hint: list the things that disappear once the compiler has parsed the call and turned arguments into values.