Preprocessor and Macros
|
This section documents C23 (ISO/IEC 9899:2024), per ISO/IEC JTC1/SC22/WG14’s freely available working draft N3220, which WG14 documents as differing from the published standard only editorially — the reference these pages are written and verified against. This content was generated with the assistance of AI and should be verified against the WG14 draft and cppreference.com’s C reference before being relied on in production. This section’s bibliography lists the reference material consulted while preparing these pages. |
The preprocessor is a text processor that runs before the compiler sees anything (translation phases 1-6, see Program Structure). It knows nothing about types, scopes or statements — which makes it powerful, and makes every macro a potential surprise.
The modern guidance is simple: use #include, include guards and conditional compilation freely; prefer
constexpr, enum, static inline and _Generic to macros for anything the compiler can express; and when
you do write a macro, follow the hygiene rules below.
Object-Like Macros
#include <stdio.h>
#define BUFFER_SIZE 4096 // a token sequence, not a typed constant
#define GREETING "hello"
#define EMPTY // expands to nothing
int main(void)
{
char buffer[BUFFER_SIZE];
printf("%s %zu\n", GREETING, sizeof buffer);
EMPTY
return 0;
}
An object-like macro is pure text substitution. It has no type, obeys no scope, and does not appear in the
debugger — which is why an enum constant or a C23 constexpr object is preferable whenever the value is
only needed by the compiler. Reserve macros for what the preprocessor itself needs (#if conditions,
string literals to concatenate, header configuration).
Function-Like Macros and Hygiene
#include <stdio.h>
// Rule 1: parenthesize EVERY parameter and the whole body.
#define SQUARE(x) ((x) * (x))
// Without those parentheses:
#define BAD_SQUARE(x) x * x // BAD_SQUARE(1 + 2) is 1 + 2 * 1 + 2 == 5
// Rule 2: never use a parameter twice -- the argument's side effects repeat.
#define MAX_UNSAFE(a, b) ((a) > (b) ? (a) : (b))
// Rule 3: wrap multi-statement macros in do { ... } while (0) so they behave
// like a single statement in every context, including an unbraced if/else.
#define LOG_AND_ADD(sum, value) \
do { \
printf("adding %d\n", (value)); \
(sum) += (value); \
} while (0)
int main(void)
{
printf("%d %d\n", SQUARE(1 + 2), BAD_SQUARE(1 + 2)); // 9 5
int i = 3;
printf("%d\n", MAX_UNSAFE(i++, 2)); // i is incremented TWICE -- avoid
printf("i = %d\n", i);
int sum = 0;
if (sum == 0)
LOG_AND_ADD(sum, 5); // works even unbraced, thanks to do/while(0)
else
LOG_AND_ADD(sum, 7);
printf("sum = %d\n", sum);
return 0;
}
The double-evaluation problem has no macro-level fix in standard C — which is the strongest argument for
static inline functions:
#include <stdio.h>
// A function evaluates each argument exactly once, is typed, and is just as fast at -O2.
static inline int max_int(int a, int b)
{
return a > b ? a : b;
}
int main(void)
{
int i = 3;
int result = max_int(i++, 2); // the argument is evaluated exactly once
printf("%d, i = %d\n", result, i); // 3, i = 4
// Note the separate statement: putting max_int(i++, 2) and i in one printf
// would be an unsequenced access to i, and undefined regardless of the callee.
return 0;
}
Naming convention: UPPER_SNAKE_CASE for macros, so a reader can see that ordinary evaluation rules may not
apply.
#include and Include Guards
// #include <stdio.h> -- angle brackets: the implementation's include path
// #include "project.h" -- quotes: the current file's directory first, then the path
Every header must be idempotent, because it will be included more than once:
#ifndef PROJECT_WIDGET_H // a unique name -- prefix it with the project
#define PROJECT_WIDGET_H
typedef struct Widget Widget; // opaque handle
Widget *widget_create(void);
void widget_destroy(Widget *w);
#endif /* PROJECT_WIDGET_H */
#pragma once does the same thing in one line and is supported by GCC, Clang and MSVC, but it is not standard
C and can misbehave when a header is reachable through symlinks or multiple mount paths. Include guards remain
the portable choice; many projects use both.
Conditional Compilation
#include <stdio.h>
#define FEATURE_LEVEL 2
int main(void)
{
#if FEATURE_LEVEL >= 3
puts("full");
#elif FEATURE_LEVEL == 2
puts("standard"); // this one is compiled
#else
puts("minimal");
#endif
#ifdef FEATURE_LEVEL // "is it defined at all?"
puts("feature level is set");
#endif
#ifndef DISABLE_LOGGING
puts("logging on");
#endif
// C23 shorthands for #ifdef/#ifndef inside an #if chain:
#if 0
puts("never");
#elifdef FEATURE_LEVEL // C23
puts("elifdef taken");
#elifndef SOMETHING_ELSE // C23
puts("not reached");
#endif
return 0;
}
Notes that matter in real headers:
-
defined(X)works inside a larger#ifexpression:#if defined(A) && !defined(B). -
An undefined identifier in an
#ifexpression evaluates to0— so a typo in a macro name silently takes the false branch.-Wundefturns that into a warning. -
#ifarithmetic uses the widest integer types and cannot usesizeof, casts, floating-point or enum constants. -
The
#if 0 … #endifblock is the correct way to comment out a region of code, since block comments do not nest.
Portability Guards
#include <stdio.h>
// Compiler detection, in the order that avoids false positives:
#if defined(__clang__)
# define COMPILER_NAME "clang"
#elif defined(__GNUC__)
# define COMPILER_NAME "gcc"
#elif defined(_MSC_VER)
# define COMPILER_NAME "msvc"
#else
# define COMPILER_NAME "unknown"
#endif
// Language-version detection: the ONLY way to tell C17 from C23.
#if defined(__STDC_VERSION__) && __STDC_VERSION__ >= 202311L
# define C_EDITION "C23"
#elif defined(__STDC_VERSION__) && __STDC_VERSION__ >= 201710L
# define C_EDITION "C17"
#elif defined(__STDC_VERSION__) && __STDC_VERSION__ >= 201112L
# define C_EDITION "C11"
#else
# define C_EDITION "C99 or earlier"
#endif
// Platform detection.
#if defined(_WIN32)
# define PLATFORM_NAME "windows"
#elif defined(__linux__)
# define PLATFORM_NAME "linux"
#elif defined(__APPLE__)
# define PLATFORM_NAME "macos"
#else
# define PLATFORM_NAME "other"
#endif
int main(void)
{
printf("%s / %s / %s\n", COMPILER_NAME, C_EDITION, PLATFORM_NAME);
return 0;
}
C23 makes two of these checks first-class:
#include <stdio.h>
// __has_include (C23; a GCC/Clang extension long before): probe for a header.
#if defined(__has_include)
# if __has_include(<stdbit.h>)
# include <stdbit.h>
# define HAVE_STDBIT 1
# else
# define HAVE_STDBIT 0
# endif
#else
# define HAVE_STDBIT 0
#endif
// __has_c_attribute (C23): probe for an attribute before using it.
#if defined(__has_c_attribute) && __has_c_attribute(nodiscard)
# define MUST_CHECK [[nodiscard]]
#else
# define MUST_CHECK
#endif
MUST_CHECK static int compute(void)
{
return 42;
}
int main(void)
{
printf("stdbit: %d, value: %d\n", HAVE_STDBIT, compute());
return 0;
}
Stringification and Token Pasting
#include <stdio.h>
// # turns an argument into a string literal; the two-level indirection is needed
// so that the argument is macro-expanded first.
#define STRINGIFY_RAW(x) #x
#define STRINGIFY(x) STRINGIFY_RAW(x)
// ## pastes two tokens into one identifier.
#define CONCAT_RAW(a, b) a##b
#define CONCAT(a, b) CONCAT_RAW(a, b)
#define VERSION 23
// A generated pair of functions -- the "X macro" family of tricks.
#define DEFINE_GETTER(type, name) \
static type CONCAT(get_, name)(void) \
{ \
return (type)0; \
}
DEFINE_GETTER(int, count)
DEFINE_GETTER(double, ratio)
int main(void)
{
printf("%s %s\n", STRINGIFY_RAW(VERSION), STRINGIFY(VERSION)); // "VERSION" "23"
printf("%d %g\n", get_count(), get_ratio());
return 0;
}
The one-argument/two-argument dance is the classic gotcha: and # suppress expansion of their operands,
so a wrapper macro is needed to expand first.
Variadic Macros and __VA_OPT__
#include <stdio.h>
// C99: at least one argument must follow the named ones.
#define LOG_C99(format, ...) fprintf(stderr, format, __VA_ARGS__)
// The GNU workaround for zero variadic arguments -- non-standard:
// #define LOG_GNU(format, ...) fprintf(stderr, format, ##__VA_ARGS__)
// C23: __VA_OPT__(x) expands to x only when __VA_ARGS__ is non-empty,
// so this works with zero extra arguments and is fully standard.
#define LOG(format, ...) \
fprintf(stderr, "[%s:%d] " format "\n", __FILE__, __LINE__ __VA_OPT__(,) __VA_ARGS__)
// The leading 0 only keeps the compound literal non-empty, so it is not counted.
#define COUNT_ARGS(...) COUNT_ARGS_IMPL(0 __VA_OPT__(,) __VA_ARGS__)
#define COUNT_ARGS_IMPL(...) (sizeof (int[]){ __VA_ARGS__ } / sizeof(int) - 1)
int main(void)
{
LOG("starting"); // zero variadic arguments -- fine in C23
LOG("value = %d", 42);
LOG("pair = %d,%d", 1, 2);
LOG_C99("%s\n", "explicit argument");
printf("%zu %zu\n", COUNT_ARGS(), COUNT_ARGS(1, 2, 3)); // 0 3
return 0;
}
Predefined Macros
#include <stdio.h>
int main(void)
{
printf("file: %s\n", __FILE__); // the source file name
printf("line: %d\n", __LINE__); // the current line number
printf("function: %s\n", __func__); // C99 -- an identifier, not a macro
printf("date/time: %s %s\n", __DATE__, __TIME__);
printf("standard C: %d\n", __STDC__);
printf("version: %ld\n", __STDC_VERSION__); // 202311L for C23
#ifdef __STDC_NO_THREADS__
puts("no <threads.h> on this implementation");
#endif
#ifdef __STDC_NO_VLA__
puts("no variable-length arrays");
#endif
#ifdef __STDC_NO_ATOMICS__
puts("no <stdatomic.h>");
#endif
return 0;
}
__func__ is technically not a macro but a predefined identifier holding the enclosing function’s name — which makes the standard logging macro:
#include <stdio.h>
#define TRACE(...) \
do { \
fprintf(stderr, "%s:%d:%s: ", __FILE__, __LINE__, __func__); \
fprintf(stderr, __VA_ARGS__); \
fputc('\n', stderr); \
} while (0)
static void work(int units)
{
TRACE("processing %d units", units);
}
int main(void)
{
work(3);
return 0;
}
#error, #warning and _Pragma
#include <limits.h>
#include <stdio.h>
// Refuse to compile on an unsupported configuration, with a readable message.
#if CHAR_BIT != 8
# error "this code requires 8-bit bytes"
#endif
#if !defined(__STDC_VERSION__) || __STDC_VERSION__ < 201112L
# error "this code requires C11 or later"
#endif
// #warning (C23; a long-standing extension before that) notes without failing:
#if 0
# warning "legacy path compiled -- migrate to the new API"
#endif
int main(void)
{
// _Pragma is the operator form of #pragma, so it can be used inside a macro:
#define DIAGNOSTIC_PUSH _Pragma("GCC diagnostic push")
#define IGNORE_UNUSED _Pragma("GCC diagnostic ignored \"-Wunused-variable\"")
#define DIAGNOSTIC_POP _Pragma("GCC diagnostic pop")
DIAGNOSTIC_PUSH
IGNORE_UNUSED
int deliberately_unused = 1;
DIAGNOSTIC_POP
puts("compiled");
return 0;
}
#error is the right way to fail fast on a bad configuration — far better than compiling and misbehaving.
#embed (C23)
#embed inserts the contents of a binary file as a comma-separated list of byte values — no build-time
conversion script, no xxd -i:
#include <stdio.h>
// The file's bytes become an initializer list.
static const unsigned char icon[] = {
#embed "icon.png"
};
// if_empty, limit, prefix and suffix are the standard parameters:
static const unsigned char header[] = {
#embed "icon.png" limit(8)
};
int main(void)
{
printf("%zu bytes, first = %02X\n", sizeof icon, header[0]);
return 0;
}
|
|
Argument Checking and Default Arguments
C has no default arguments, but the preprocessor can fake them — and _Generic does it better:
#include <stdio.h>
// Count-based dispatch: pick a macro by the number of arguments.
#define GET_MACRO(_1, _2, _3, NAME, ...) NAME
#define CONNECT(...) GET_MACRO(__VA_ARGS__, CONNECT3, CONNECT2, CONNECT1)(__VA_ARGS__)
#define CONNECT1(host) connect_full((host), 80, 30)
#define CONNECT2(host, port) connect_full((host), (port), 30)
#define CONNECT3(host, port, timeout) connect_full((host), (port), (timeout))
static int connect_full(const char *host, int port, int timeout)
{
printf("connect %s:%d timeout=%d\n", host, port, timeout);
return 0;
}
int main(void)
{
CONNECT("example.com");
CONNECT("example.com", 8080);
CONNECT("example.com", 8080, 5);
return 0;
}
This works, and it is exactly the kind of cleverness to use sparingly: the error messages are terrible and the
macro cannot be stepped through. A plain connect_full plus a small wrapper function is usually the better
engineering choice. For type-based rather than count-based dispatch, see
Type-Generic Programming.
See Also
-
Program Structure — the translation phases the preprocessor runs in.
-
Type-Generic Programming —
_Generic,typeofand the type-safe alternatives to macros. -
Constants, Enumerations and Initialization —
constexprandenuminstead of#define. -
C Standards and C23 — feature-test macros and writing code for several editions.
-
C++: Namespaces, Modules, and the Preprocessor — C++ keeps the same preprocessor but moves most of its jobs into the language.
References
-
WG14 N3220 — the C23 working draft (§6.10 "Preprocessing directives", §6.10.3 "Macro replacement", §6.10.3.1
__VA_OPT__, §6.10.4 "Binary resource inclusion"). -
Clang — Language Extensions (
has_include,has_c_attribute).