Preprocessor
C/C++ Preprocessor Features
The C preprocessor is used by both C and C++. It is an indispensable phase in the compilation of programs in these languages, due to their separate compilation design. This article covers the preprocessor in depth, with examples in C, and discusses C++ alternatives and modern additions throughout. It also covers new facilities introduced in C++17, C++20, C++23, and C23.
PREREQUISITES — You should already…
- know how to compile C/C++ programs and understand the compilation process.
- have a fundamental understanding of C/C++ structure, statements, and types.
Overview
Although the C preprocessor is just a text manipulator, it is still very useful — if not indispensable. It can be used to reduce or eliminate code duplication, and also to create conditional code, which can greatly enhance the portability of a program.
The preprocessor runs before the compiler proper and has no knowledge of C or C++ types, scopes, or semantics. What it produces — the fully expanded source text — is called a compilation unit or translation unit, and it is this output that the compiler actually compiles. Because the preprocessor is a pure text processor, it is both powerful and dangerous: it can generate arbitrary code, but it does so blindly, without any of the safety nets the compiler provides.
From C++11 onward, many of the preprocessor's traditional roles have
been taken over by language features — constexpr for
constants, templates for generic functions, static_assert
for compile-time checks. The preprocessor nonetheless remains
indispensable for certain tasks that the language cannot express:
platform and compiler detection, conditional header inclusion,
build-time feature flags, and extern "C" guards for mixed
C/C++ headers. This article covers the full picture, pointing out the
modern C++ alternative wherever one exists.
Here are some of the fundamental properties of the preprocessor:
The preprocessor manipulates text only. Its output, called a compilation unit, is the only source that is compiled.
Directives start with a
#(hash / pound) character. The#must be the first non-whitespace character on the line, but may be preceded by whitespace.The
#is followed by the name of the directive. Whitespace may separate the#and the name, though this is uncommon in practice.Preprocessor directives are terminated by a newline. The backslash character followed immediately by a newline is a line continuation, which allows directives to span several lines.
A
#by itself is the null directive and has no effect.The preprocessor recognises C/C++ tokens, but assigns no meaning to them. It does not know anything about functions — not their names, not their scope, not their types.
Scopes and blocks have no meaning to the preprocessor.
Macro names live in their own symbol table, separate from C/C++ identifiers. A macro name may even shadow a keyword.
Macro expansion is not recursive. The preprocessor scans the output of each expansion for further macros, but it will never re-expand the macro that initiated the expansion. This deliberately prevents infinite recursion:
#define X Xexpands toXand stops.
Technically speaking, the preprocessor is not strictly necessary to create any program, but omitting it from the compilation process would be extremely inconvenient and would result in extensive manual duplication of code.
Directive List
For convenience, here is a list of all the standard directives, with a short description of each. See also the GNU Preprocessor Manual.
| Directive | Description |
|---|---|
#define |
Macro directive. Macros are
expanded, and their existence can be tested. Macros
can be created on the command line with a compiler driver switch (often:
-D‹macro›, or:
-D‹macro›=‹expansion›). The
expansion text may optionally contain the preprocessor operators
# and ##. |
#undef |
Removes macros created with
#define, including macros created on the command line. |
#error |
Emits a compiler error with the given message text and halts preprocessing. Most useful inside conditional directives, to catch invalid or mutually exclusive macro configurations at build time. |
#warning |
Emits a compiler warning with the given message text and continues. Standardised in C++23 and C23. Supported as a compiler extension by GCC, Clang, and MSVC for many years before standardisation. |
#include |
Replaces the directive line with the
entire contents of the named file, which is itself preprocessed. The
path separator should always be a forward slash (/), even
on Windows®. Use <angle brackets> for
system and library headers; use "double
quotes" for project headers. |
#pragma |
Provides compiler-specific options.
Compilers that do not recognise the syntax following
#pragma are required to ignore it. |
#if |
Opens a conditional block. Evaluates a
constant integer expression; undefined macro names expand to
0. Logical and comparison operators are allowed. Supersedes
#ifdef and #ifndef for non-trivial conditions.
Must be closed by #endif. |
#elif |
"Else if" — has the same expression
capabilities as #if. May only follow a #if or
another #elif. |
#elifdef |
"Else if defined" — shorthand for
#elif defined. Standardised in C++23 and C23. |
#elifndef |
"Else if not defined" — shorthand for
#elif !defined. Standardised in C++23 and C23. |
#else |
Optional final branch. May only appear
once before the #endif of a
#if/#ifdef/#ifndef group. |
#endif |
Closes a #if,
#ifdef, or #ifndef block. |
#ifdef |
True if the named macro is defined.
Equivalent to #if defined‹macro›. |
#ifndef |
True if the named macro is not
defined. Equivalent to #if !defined‹macro›. |
#line |
Changes the line number (and optionally the filename) reported in subsequent compiler diagnostics. Rarely written by hand; commonly emitted by code generators such as yacc/bison. |
NOTE — Conditional Directives
The directives #if, #ifdef,
#ifndef, #elif, #elifdef,
#elifndef, #else, and #endif
together constitute the conditional directives. They can be
nested arbitrarily, and should be indented to make the nesting structure
visible. The #elifdef and #elifndef shortcuts
were added in C++23 and C23 — use them freely in new code.
Macro Directive
The #define directive creates a macro — an
identifier that is expanded to a substitution text at every
point it appears in the source. The preprocessor performs this
substitution before the compiler sees the code. Macros come in two
forms: object-like (no parameters) and function-like
(with a parameter list).
Syntax — Macro Directive
#definemacro
Defines macro with an empty expansion. Same as-Dmacro on the command line.#definemacro1
Defines macro expanding to1. Same as-Dmacro=1on the command line.#definemacro expansion
Defines macro expanding to expansion.#definemacro(parm-list)expansion
Function-like macro. The opening parenthesis must immediately follow the macro name — no whitespace is permitted. Used as: macro(arg-list)- macro ⇒ any legal identifier.
- expansion ⇒ any text; may reference names from parm-list.
- parm-list ⇒ comma-separated list of
identifiers, optionally ending with
.... - arg-list ⇒ values 'assigned' to parameters in parm-list.
Long macros can be continued on the following line using line-continuation (a trailing backslash before the newline).
Macros are not expanded inside string literals, nor when the macro name appears as part of a larger token.
The expansion of a macro is itself scanned by the preprocessor for
further macros, which are expanded in turn. However, the macro being
expanded is not re-expanded — this prevents recursive
expansion. The following will expand to X and stop:
Macros are not recursively expanded
#define X X
XThis rule has a practical consequence: if a macro argument contains
the macro's own name, it will not be expanded. The same rule applies to
the ## token-paste operator — arguments appearing next to
## are pasted before being expanded, so any macro
name in the argument survives unexpanded into the new token.
The standard workaround is the redirection trick: add one level of indirection by calling a helper macro. The outer macro's arguments are fully expanded before the helper sees them, because by that point the originating macro is no longer "active" and the no-re-expansion guard no longer applies:
Redirection trick — force expansion before token paste
#define CONCAT_(a, b) a##b // pastes without pre-expanding
#define CONCAT(a, b) CONCAT_(a, b) // outer macro expands a and b first
#define PREFIX my_
CONCAT_(PREFIX, func) // → PREFIX_func — PREFIX not expanded (pasted directly)
CONCAT(PREFIX, func) // → my_func — PREFIX expanded first, then pastedThe same trick applies to stringification (#) and to any
situation where you need a macro argument to be fully expanded before
the operator sees it. You will encounter this pattern frequently in
non-trivial macro code.
TIP — Macro Expansion Experiments
You can experiment with macro expansion using GCC or Clang. Write your macros and test input in any
text file, then pipe it through gcc -E -P - to see the
expanded output:
Unix-like shell or PowerShell
cat
macros.txt | gcc -E -P -The -E flag stops after preprocessing; -P
suppresses line markers. You can also preprocess a source file directly
to a .i file:
gcc
-E -o translation_unit.i source.cSymbolic Constants
Since C has no compile-time constant mechanism equivalent to C++'s
constexpr, symbolic constants in C are traditionally
defined with #define:
Symbolic constant in C
#define PI 3.14159265358979323846
#define MAX_USERS 256
#define APP_NAME "MyApp"These macros have no type, no scope, and are invisible to the debugger. They are replaced by their expansion text before the compiler sees any code.
In C++11 and later, you should instead use constexpr
variables, which are typed, scoped, and fully visible to the debugger
and static analysis tools:
Symbolic constants in C++11
constexpr double PI = 3.14159265358979323846;
constexpr int MAX_USERS = 256;
constexpr const char* APP_NAME = "MyApp";
// Or, better still, for string constants in C++17:
constexpr std::string_view APP_NAME = "MyApp";The C++ versions can be placed in header files without any
disadvantage — a constexpr variable at namespace scope has
internal linkage by default, so including the header in multiple
translation units is safe.
Note — Optional Mathematical Constants
Compilers like GCC, Clang, and MSVC support a convention that provides mathematical
constants such as M_PI when you define
_USE_MATH_DEFINES before including
<cmath> (C++) or <math.h> (C). In
C++20, the standard library provides std::numbers::pi and
related constants in <numbers>, making this
convention unnecessary for C++ code.
// C++ 20
#include <numbers>
constexpr double tau = 2.0 * std::numbers::pi;Inline Generic Functions
Both C99 and C++ offer the inline modifier. In C,
however, the language is statically typed and has no templates, so
macros with parameters are frequently used to write functions that work
with multiple types:
Macros with zero or more parameters
#define ANSI_CLS() (fprintf(stderr, "\x1B[2J\x1B[0;0H"))
#define SQR(x) ((x) * (x))
#define MAX(a, b) ((a) > (b) ? (a) : (b))The macros are used exactly as if they were functions:
Using macros taking zero or more arguments
ANSI_CLS();
double d = SQR(2.5);
int i = SQR(25);
long l = SQR(25L);SQR() works with any arithmetic type — the result type
is the same as the argument type. This is the principal advantage of
function-like macros over ordinary typed functions in C.
The danger is double evaluation: every macro argument is substituted literally at every point it appears in the expansion. If the argument has side effects, those side effects occur multiple times:
Double evaluation pitfall
int n = 3;
int r = SQR(++n); // expands to ((++n) * (++n)) — n incremented twice, UBAlways parenthesise every parameter in the expansion, and every expansion as a whole:
Correct parenthesisation
#define SQR(x) ((x) * (x)) // every use of x, and the whole expansion
#define ADD(a, b) ((a) + (b))As a GCC/Clang extension (non-standard),
statement expressions allow you to avoid double evaluation in C, using
({ ... }) syntax. The argument is assigned to a local
variable of the same type via __typeof__, evaluated once,
and the local variable is used in the computation:
Safer, non-portable SQR for GCC/Clang
#define SQR(x) ({ __typeof__(x) x_ = (x); x_ * x_; })Now SQR(++n) evaluates ++n exactly once.
Note that __typeof__ and statement expressions are
GCC/Clang extensions — this code is not portable to MSVC or to strict
standard C.
In C++, avoid function-like macros entirely. Use inline
template functions:
C++ alternative to function-like macros
template <typename T>
inline constexpr auto sqr(T x) noexcept -> T {
return x * x;
}
template <typename T>
inline constexpr auto max_val(T a, T b) noexcept -> T {
return a > b ? a : b;
}The compiler handles syntax and type checking; the debugger can step into the function; there is no double-evaluation; and unlike the macro, this produces informative error messages for invalid argument types. In production C++ code, only macro names should be written in all-capitals — template functions are normal identifiers.
Macros Expanding to Multiple Statements
When a macro expands to more than one statement, the expansion must
be wrapped in a do { ... } while(0) construct. Without
this, the macro breaks silently when used as the body of an
if without braces:
Broken multi-statement macro
#define LOG_ERR(msg) \
fprintf(stderr, msg); \
error_count++ //← two statements — second always executes.
if (failed)
LOG_ERR("write error"); //← only fprintf is guarded by if
else // syntax error: else without matching if
recover();Correct multi-statement macro
#define LOG_ERR(msg) \
do { \
fprintf(stderr, msg); \
error_count++; \
} while (0)The do/while(0) expands to a single
statement, accepts a trailing semicolon naturally, and does not confuse
if/else parsing. This pattern is extremely
common — use it unconditionally whenever a macro expands to more than
one statement.
Include / Header Files
The #include directive replaces the directive line with
the entire contents of the named file. That content is then preprocessed
in turn — it may contain further #include directives, macro
definitions, and conditional code. Deep nesting of includes can become
difficult to maintain and should be controlled carefully in a
project.
Syntax — Include Directive
#include<filename>
Search the compiler's system and library include paths. Use for standard library and third-party headers.#include"filename"
Search relative to the current file first, then fall back to the system paths. Use for your own project headers.
The path separator in filename should always
be a forward slash (/), even in code written on or for
Windows™.
Header files should not contain definitions with external
linkage. No global variable definitions, and no
non-inline function definitions, should appear in a header.
Such code would be compiled into every translation unit that includes
the header, causing linker errors (multiple definition) or silent ODR
violations. Headers should contain declarations, inline
functions, constexpr variables, templates, and type
definitions.
Include Guards
Because headers can be included from multiple places, and because a header may include other headers, any given header can easily be processed more than once during a single compilation. Include guards prevent this.
The modern approach: #pragma once
#pragma once
// contents of header#pragma once is not in the ISO C or C++ standard, but it
is supported by every mainstream compiler (GCC,
Clang, MSVC, ICC, and others) and has been for
over fifteen years. It is simpler, less error-prone, and often faster
than traditional guards. Use it for all new code, unless you are
specifically targeting environments where compiler support is uncertain
(deeply embedded toolchains, obscure C compilers).
Traditional include guards
When portability to non-mainstream compilers is required, use the traditional pattern:
#ifndef MYHEADER_H
#define MYHEADER_H
// contents of header
#endif // MYHEADER_HThe guard macro name should be derived from the filename. Avoid names
with a leading underscore followed by an uppercase letter
(_MYHEADER_H_) — the C and C++ standards reserve such names
for the implementation. A simple MYHEADER_H or
MYHEADER_H_INCLUDED is safe.
You may also see the #if !defined form, which has the
advantage of supporting compound conditions (though that is rarely
needed for a guard):
#if !defined MYHEADER_H
#define MYHEADER_H
// ...
#endifUUIDs as Guard Macros
If you want a guard that is immune to file renames, you can use a UUID as the macro
name. Generate one on the command line and prefix it with H
(macro names may not start with a digit):
echo
H`uuidgen` | tr -d '-' | tr '[:lower:]' '[:upper:]'Or in PowerShell on any platform:
"H" +[guid]::NewGuid().ToString().ToUpper().Replace("-","")H34C3C14AE5C45CE96C8B31312186B72
#ifndef H34C3C14AE5C45CE96C8B31312186B72
#define H34C3C14AE5C45CE96C8B31312186B72
// contents of header
#endifGenerate your own — do not reuse the above UUID.
Predefined Macros
A number of preprocessor macros are predefined by the compiler. These macros may not be redefined or undefined by user code.
Standard C Macros
| Macro | Description |
|---|---|
__DATE__ |
Compilation date as a string literal in
the format: "Jun 6 2026". Single-digit days are
space-padded, not zero-padded. |
__TIME__ |
A string literal ("13:05:03")
containing the time at which preprocessing began. |
__FILE__ |
A string literal containing the name of the current source file. |
__LINE__ |
An integer literal (int)
representing the current line number in the file. |
__STDC__ |
Expands to 1 if the compiler
conforms to the ISO C standard. |
__STDC_HOSTED__ |
Expands to 1 in a hosted
environment (standard OS); 0 in a freestanding environment
(bare metal, kernel). |
__STDC_VERSION__ |
A long literal identifying
the C standard in effect: 199901L (C99),
201112L (C11), 201710L (C17),
202311L (C23). |
Three macros are conditionally defined:
| Macro | Description |
|---|---|
__STDC_IEC_559__ |
Expands to 1 if the
floating-point implementation conforms to IEC 60559 (identical to IEEE
754). |
__STDC_IEC_559_COMPLEX__ |
Expands to 1 if the complex
floating-point implementation conforms to IEC 60559. |
__STDC_ISO_10646__ |
Expands to a long date
literal up to which the compiler conforms to ISO/IEC 10646 (Unicode) for
wchar_t. |
A community-maintained, comprehensive list of predefined macros (including many non-standard, compiler-specific macros) can be found at github/cpredef.
C++ Macros
| Macro | Description |
|---|---|
__cplusplus |
Defined by any C++ compiler. Expands to a
long literal identifying the C++ standard in effect:
199711L (C++98/03), 201103L (C++11),
201402L (C++14), 201703L (C++17),
202002L (C++20), 202302L (C++23). See note
below regarding MSVC. |
__cpp_* |
Feature-test macros. Defined by C++20-conforming implementations to indicate the availability of specific language and library features. See the Feature Detection section. |
NOTE — __cplusplus and
MSVC
MSVC historically reported
__cplusplus as 199711L regardless of the
actual C++ standard being compiled, unless the
/Zc:__cplusplus flag was explicitly passed. Always include
this flag in new MSVC projects:
cl
/Zc:__cplusplus /std:c++latest ···The most common use of __cplusplus is to guard syntax
that C does not understand, in headers shared between C and C++:
#if defined __cplusplus
extern "C" {
#endif
// C function declarations
#if defined __cplusplus
}
#endifTIP — Listing Predefined Macros with GCC or Clang
With gcc,
g++, clang,
or clang++, you can print all predefined
macros with:
echo
| gcc -dM -E -The trailing dash (-) reads from standard input; the
empty echo provides a null source file. In PowerShell, use
two single quotes instead of echo:
''
| gcc -dM -E -All compilers define additional, compiler-specific macros which can be used to identify the compiler, the target architecture, and the operating system. This is essential for portable, cross-platform code.
Conditional Compilation
Conditional compilation is one of the preprocessor's most valuable features — and the one that nothing in C++ can replace. By changing the compilation command line (or IDE configuration, or Makefile rules), you can include or exclude entire sections of code. This is used for debugging code, platform portability, feature flags, and API versioning.
Basic Patterns
#ifdef DEBUG
log_message("entered f()");
#endif
#ifndef NDEBUG
validate_invariants();
#endif
#if defined __linux__
use_epoll();
#elif defined __APPLE__
use_kqueue();
#elif defined _WIN32
use_iocp();
#else
#error "Unsupported platform"
#endifConditionals Comparision
‘#ifdef ‹macro›’ is exactly equivalent to
‘#if defined ‹macro›’, and #ifndef
‹macro›’ to ‘#if !defined ‹macro›’. The
advantage of #if defined is that it supports logical
operators, allowing multiple conditions in a single expression:
Compound
conditions only possible with #if defined
#if !defined NDEBUG || defined DEBUG || defined _DEBUG
// any debug configuration
#endif
#if defined __STDC__ && __STDC_VERSION__ >= 199901L
// C99 or later
#endif
#if defined __cplusplus && __cplusplus >= 202002L
// C++20 or later
#endifUse #ifdef / #ifndef for simple
single-macro existence checks; use #if defined for anything
more complex.
Undefined Macros in
#if
When a macro name appears in a #if or #elif
expression but has not been defined, it expands to 0 —
which #if treats as logically false. This means
#if FEATURE_X is silently false when FEATURE_X
is not defined, rather than being an error. Use
#if defined FEATURE_X when you want to distinguish ‘defined
to zero’ from ‘not defined at all’.
#if FEATURE_X // FEATURE_X undefined → expands to 0 → falseThis is why -DFEATURE_X (defining to empty) and
-DFEATURE_X=1 (defining to 1) behave
differently in a #if FEATURE_X test. An empty expansion is
not a valid integer constant and may cause a diagnostic; 1
is unambiguous. For macros used as boolean flags, prefer:
#define FEATURE_X 1 // or -DFEATURE_X=1 on the command lineC++23/C23 Conditionals
These shortcuts reduce verbosity in multi-branch platform and feature detection:
Without the new shortcuts
#ifdef PLATFORM_LINUX
use_epoll();
#elif defined PLATFORM_MACOS
use_kqueue();
#elif !defined PLATFORM_WINDOWS
#error "Unknown platform"
#else
use_iocp();
#endifWith C++23/C23 shortcuts
#ifdef PLATFORM_LINUX
use_epoll();
#elifdef PLATFORM_MACOS
use_kqueue();
#elifndef PLATFORM_WINDOWS
#error "Unknown platform"
#else
use_iocp();
#endifC++23/C23 Warning Directive
Emits a non-fatal diagnostic message. Useful for deprecation notices, incomplete implementations, or build-configuration advisories:
#ifndef __cpp_concepts
#warning "Concepts unavailable — falling back to SFINAE constraints"
#endif
#if __cplusplus < 202002L
#warning "This header works best with C++20 or later"
#endif#warning has been supported as a compiler extension by
GCC, Clang, and MSVC for
many years and was standardised in C++23 and C23.
Debug/Release Convention
The NDEBUG macro is the only standard mechanism
for distinguishing between debug and release compilations. Its presence
means "no debugging":
#ifndef NDEBUG
fprintf(stderr, "[debug] cache miss for key %d\n", key);
#endifThe assert() macro (covered below) also uses
NDEBUG. In a release build, pass -DNDEBUG on
the command line or define it in your build system configuration; in a
debug build, leave it undefined.
Preprocessor Operators
The preprocessor defines four operators of its own, distinct from the
C/C++ arithmetic, comparison, and logical operators that
#if and #elif can use in their constant
expressions. The preprocessor-specific operators are:
| Operator | Description |
|---|---|
defined |
Tests for the existence of a macro. Used
after #if or #elif only. Returns "true" if the
macro exists. Parentheses around the macro name are optional:
defined FOO and defined(FOO) are
equivalent. |
# |
Stringify operator (also called
stringification or stringisation). Surrounds the
expanded macro argument with double quotes, producing a string literal.
Valid only inside a #define expansion. |
## |
Token paste operator.
Concatenates two adjacent tokens into one new token. Valid only inside a
#define expansion. |
_Pragma |
Pragma operator.
_Pragma("‹args›") is equivalent to
#pragma ‹args›. Standardised in C99 / C++11. The
only standard way to emit a pragma from within a macro expansion, since
macros cannot otherwise generate directives. |
NOTE — Charisation Operator
The charisation operator (#@) is a Visual
C/C++™ extension that places single quotes around a macro argument. It
is non-standard and not particularly useful — it is mentioned here for
completeness only.
Defined
The defined operator is used with #if or
#elif to test for a macro's existence. It returns "true" (a
non-zero integer) if the macro is defined, and "false" (zero)
otherwise:
#if defined versus
#ifdef
#if !defined NDEBUG // equivalent to: #ifndef NDEBUG
#if defined __linux__ && defined __x86_64__ // #ifdef cannot do thisThe superiority of #if defined over #ifdef
is that logical operators can combine multiple defined
tests in a single expression:
Complex conditional using
defined
#if !defined NDEBUG || defined DEBUG || defined _DEBUG
// compiled in any debug configuration
#endifStringify
The stringify operator (#param)
places double quotes around the expanded text of a macro parameter, producing a string literal. It is only
meaningful in function- like macros, since it requires a parameter to
operate on.
The most common use is in debugging macros: given a variable name,
you can produce both the name as a string and its value in a single
macro invocation. The following DBGVAR macro uses
stringification and implicit string literal concatenation to format and
print the name and value of any variable, together with the source
location:
Stringification in a debug variable macro
#ifdef NDEBUG
#define DBGVAR(v, f) ((void)0)
#else
#define DBGVAR(v, f) \
fprintf(stderr, "%-12s = %" #f " (%s:%d)\n", \
#v, (v), __FILE__, __LINE__)
#endif
int i = 123;
double d = 4.56;
char* s = "ABCDEF";
DBGVAR(i, d); // i = 123 (main.c:42)
DBGVAR(d, .2f); // d = 4.56 (main.c:43)
DBGVAR(s, s); // s = ABCDEF (main.c:44)The #f produces a format string from the second
argument; #v produces the variable name; (v)
is the variable value. The implicit concatenation of adjacent string
literals assembles the full format string at compile time.
If you prefer C++ style output for the debug macro:
C++ debug macro using iostreams
#ifdef NDEBUG
#define DBGVAR(v) ((void)0)
#else
#define DBGVAR(v) \
std::cerr \
<< std::setw(12) << std::left << #v " = " \
<< (v) << " (" __FILE__ ":" << __LINE__ << ')' \
<< '\n'
#endifThis version uses <iostream> and
<iomanip>. Note that __FILE__ and the
surrounding string literals are concatenated at compile time;
__LINE__ is an integer and must go through
<<.
NOTE — Stringification and Re-expansion
An important subtlety: when # is applied to a parameter,
the argument is not macro-expanded before stringification. If
you want to stringify the expansion of a macro, use a level of
indirection:
#define STRINGIFY_(x) #x
#define STRINGIFY(x) STRINGIFY_(x) //← x expanded before STRINGIFY_
#define VERSION 42
STRINGIFY(VERSION) // → "42" (expanded first)
STRINGIFY_(VERSION) // → "VERSION" (not expanded)Token Concatenation
The token paste/concatenation operator (token##token)
joins two tokens, often a prefix and a suffix, into a single new token.
Only really useful if at least one of the tokens is a macro parameter. Generally useful for
generating function, variable, or type names programmatically.
Basic token concatenation
extern void FA(void);
extern void FB(void);
#define CALL(a, b) a##b()
CALL(F, A); // → FA()
CALL(F, B); // → FB()Since ## produces a brand new token, the result is
not re-examined for macro expansion. If you want the arguments
to be expanded before pasting, add one level of indirection —
this is called the argument pre-scan rule:
Pre-scan indirection for token paste
#define X F
#define CALL(a, b) a##b() // X is pasted directly, not expanded first
CALL(X, A); // → XA() — probably not what you want
#define CALL_(a, b) a##b()
#define CALL(a, b) CALL_(a, b) // a and b are fully expanded before CALL_ sees them
CALL(X, A); // → FA() — X expanded to F, then pasted with AThis indirection pattern appears frequently in advanced macro code. The GCC preprocessor has historically had non-standard behaviour in this area; the indirection ensures consistent results across all preprocessors.
Token concatenation is most valuable for generating type-specific function names from a type tag — a pattern commonly used in C to emulate the generic programming that C++ handles with templates:
Generating type-specific functions with token paste
#define MAKE_SWAP(T) \
void swap_##T(T* a, T* b) { \
T t = *a; *a = *b; *b = t; \
}
MAKE_SWAP(int) // generates: void swap_int(int* a, int* b) { ... }
MAKE_SWAP(double) // generates: void swap_double(double* a, double* b) { ... }
MAKE_SWAP(size_t) // generates: void swap_size_t(size_t* a, size_t* b) { ... }You can also create a declaration macro to match:
Declaration macro for generated functions
#define DECL_SWAP(T) extern void swap_##T(T*, T*)
DECL_SWAP(int);
DECL_SWAP(double);In C++, use a template function instead — it is handled by the compiler, supports any type, and requires no per-type instantiation:
C++ template alternative
template <typename T>
inline auto swap_vals(T& a, T& b) noexcept -> void {
T t = std::move(a);
a = std::move(b);
b = std::move(t);
}Or simply use std::swap() from
<utility>, which does exactly this. The C++ standard
library has done the work for you.
Pragma Operator (C99 / C++11)
Because macros cannot generate preprocessor directives, the
_Pragma operator was introduced as the standard way to emit
a #pragma from within a macro.
_Pragma("‹string›") is equivalent to
#pragma ‹string›, but can appear in a macro
expansion.
The most practical use is creating portable warning suppression macros:
Warning
suppression macros using _Pragma
#if defined __GNUC__ // covers GCC and Clang
#define SUPPRESS_WARNING(w) \
_Pragma("GCC diagnostic push") \
_Pragma("GCC diagnostic ignored " #w)
#define RESTORE_WARNING \
_Pragma("GCC diagnostic pop")
#elif defined _MSC_VER
#define SUPPRESS_WARNING(w) \
__pragma(warning(push)) \
__pragma(warning(disable: w))
#define RESTORE_WARNING \
__pragma(warning(pop))
#else
#define SUPPRESS_WARNING(w)
#define RESTORE_WARNING
#endif
SUPPRESS_WARNING("-Wold-style-cast")
legacy_c_api(); // known to trigger the warning
RESTORE_WARNINGNote that MSVC provides
__pragma(...) as its own (non-standard) equivalent of
_Pragma. The portable version above wraps both.
Miscellaneous Directives
Line Directive
The #line directive changes the line number — and
optionally the filename — that the compiler reports in subsequent
diagnostics:
#line 100 "mytemplate.tmpl"Programmers almost never write #line directly. It is
emitted by code generators (yacc/bison, flex, m4, and similar tools) to
point error messages back to the original template or grammar file,
rather than to the generated C source.
Pragma Directive
The #pragma directive provides a standard hook for
compiler-specific options. A compiler that does not recognise a
#pragma is required to ignore it, which makes pragmas a
relatively safe extension mechanism.
The C99 standard defines three pragmas relating to the floating-point environment:
#pragma STDC FP_CONTRACT‹ON‖OFF‖DEFAULT›#pragma STDC FENV_ACCESS‹ON‖OFF‖DEFAULT›#pragma STDC CX_LIMITED_RANGE‹ON‖OFF‖DEFAULT›
These are specialised and rarely seen in general-purpose code. All other pragmas are implementation-defined. The most commonly needed:
Warning suppression — GCC/Clang
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wformat"
// ... code with a known format mismatch ...
#pragma GCC diagnostic popWarning suppression — MSVC
#pragma warning(push)
#pragma warning(disable: 4996) // 'function': was declared deprecated
// ...
#pragma warning(pop)Structure packing, when supported*
#pragma pack(push, 1)
typedef struct {
uint8_t type;
uint32_t length;
uint8_t data[1];
} WireHeader;
#pragma pack(pop)Pragma Once
#pragma once is the modern, recommended way to guard a
header file against multiple inclusion:
#pragma once
// contents of headerAlthough not in the ISO standard, it is supported by every mainstream compiler and has effectively become universal. It is simpler than traditional include guards and avoids the risk of guard macro name collisions. Use it for all new C and C++ headers.
Function Name Identifier
Although frequently used alongside __FILE__ and
__LINE__, __func__ is not a preprocessor
macro. It is a C99 language feature: a predefined identifier that
behaves as a static const char[] containing the undecorated
name of the current function. The preprocessor has no knowledge of
it.
void connect(const char* host, int port) {
fprintf(stderr, "[%s] connecting to %s:%d\n", __func__, host, port);
}GCC and Clang also provide
__FUNCTION__ (identical to __func__) and
__PRETTY_FUNCTION__, which includes the full function
signature — useful for disambiguating overloaded C++ functions. MSVC provides __FUNCSIG__ (decorated
signature) and __FUNCDNAME__ (mangled name).
In C++20, std::source_location provides access to file,
line, and function name within function signatures — without needing a
macro at all:
cpp — C++20 source location
#include <source_location>
void log(std::string_view msg,
std::source_location loc = std::source_location::current()) {
std::println(stderr, "[{}:{}:{}] {}",
loc.file_name(), loc.line(), loc.function_name(), msg);
}Assertions
Assertions are used to create abstractions for preconditions and postconditions.
The assert() macro from
<cassert> (C++) or <assert.h> (C)
is not built into the preprocessor — it is a standard library macro that
expands to a runtime check in debug builds and to nothing in release
builds.
cpp — C++ pre/post-condition example
#include <cassert>
void my_str_copy(char* dest, const char* src) {
// Precondition: neither pointer may be null.
assert(dest != nullptr && src != nullptr);
while ((*dest++ = *src++))
;
// Postcondition: destination must equal the source.
assert(strcmp(dest - (strlen(src) + 1), src - (strlen(src) + 1)) == 0);
}When NDEBUG is defined (a release build), every
assert() expands to ((void)0) — zero overhead,
zero code. This is why you should never put code with side
effects inside assert():
assert(n++ < max); // n is not incremented in a release build — bug
assert(connect()); // connect() not called in release — catastrophic bugA useful convention for more informative assertion failures: the
&& operator short-circuits, and a non-null string
literal is always truthy, so combining an assertion with a string
message works portably:
Assertion with embedded message
assert(index < size && "index must be less than size");
assert(ptr != nullptr && "pointer must not be null before dereferencing");The message appears in the failure output without any additional macro machinery.
Use assert() to document and enforce programmer
invariants — conditions that must hold if and only if the code is
correct. Do not use it for user input validation, recoverable errors, or
anything that must also work in release builds. The simplification of
what assert.h does internally:
Simplified internals of assert.h
#ifndef NDEBUG
#define assert(x) /* runtime check — expand to something useful */
#else
#define assert(x) ((void)0)
#endifStatic Assertions
From C++11/C11, the static_assert is a language
keyword, not a preprocessor macro, but it is often discussed
alongside assert() because it serves a similar role at
compile time. It produces a compilation error if its boolean expression
evaluates to false, and generates no code at all.
cpp — Static assertions in C11/C++11
static_assert(sizeof(long) == 8, "expected 64-bit long");
static_assert(sizeof(int) >= 4); // message is optional (C++17)
template <typename T>
void serialise(T val) {
static_assert(std::is_trivially_copyable_v<T>,
"T must be trivially copyable to serialise into a byte stream");
std::memcpy(buffer_, &val, sizeof(T));
}The C11 version of static_assert from
<assert.h> (which expands to the
_Static_assert keyword) is somewhat less powerful than the
C++11 version, but is nonetheless useful — provided you are compiling
with C11 or later. In C23, the message argument is optional, matching
C++17 behaviour.
static_assert has nothing to do with the preprocessor —
it operates at the language level and can test any compile-time constant
expression, including type traits, sizeof, and
constexpr functions. See also [[C++ Compile-Time
Computation]].
Variadic Macros (C99 / C++11)
A macro parameter list may end with ..., making the
macro variadic. The additional arguments are accessed inside
the expansion with the special identifier __VA_ARGS__.
Variadic macros were introduced in C99 primarily to support macros that
wrap variadic functions such as printf().
Debug message macro using variadic arguments
#define DBGMSG(fmt, ...) \
fprintf(stderr, "(%s:%d) " fmt, __FILE__, __LINE__, __VA_ARGS__)
int i = 123;
double d = 4.56;
DBGMSG("i = %d, d = %f\n", i, d);
// → fprintf(stderr, "(%s:%d) i = %d, d = %f\n", "main.c", 10, i, d);Empty Variadic Arguments
If a variadic macro is called with no arguments after the last fixed
parameter, __VA_ARGS__ expands to nothing, leaving a
trailing comma in the expansion, which will be a syntax error:
DBGMSG("message\n");
// → fprintf(stderr, "(%s:%d) message\n", "main.c", 15, );
// ^— syntax errorSeveral solutions exist, with different portability characteristics.
GCC/Clang extension —
, ##__VA_ARGS__
The GCC and Clang preprocessors (and the conforming MSVC preprocessor with /Zc:preprocessor)
support a non-standard extension: if the pattern
, ##__VA_ARGS__ appears in a macro expansion and
__VA_ARGS__ is empty, the preceding comma is deleted:
#define DBGMSG(fmt, ...) \
fprintf(stderr, "(%s:%d) " fmt, __FILE__, __LINE__, ##__VA_ARGS__)This works with GCC, Clang, and MSVC with /Zc:preprocessor, but is not
portable to all C compilers.
Standard C++20/C23 Solution —
__VA_OPT__
__VA_OPT__(‹tokens›) expands to ‹tokens›
when __VA_ARGS__ is non-empty, and to nothing when
__VA_ARGS__ is empty. This is the clean, portable, standard
solution for C++20 and C23:
Variadic macro using
__VA_OPT__
#define DBGMSG(fmt, ...) \
fprintf(stderr, "(%s:%d) " fmt, __FILE__, __LINE__ __VA_OPT__(,) __VA_ARGS__)
DBGMSG("checkpoint\n"); // no extra args — __VA_OPT__(,) → nothing
DBGMSG("x = %d\n", x); // has extra args — __VA_OPT__(,) → ,In C++20 / C23 code, prefer __VA_OPT__ over the
, ## extension.
NOTE — MSVC and Conforming Preprocessor
The traditional MSVC preprocessor interprets
__VA_ARGS__ differently from GCC/Clang and does not support the
, ##__VA_ARGS__ extension. The conforming MSVC preprocessor — enabled with
/Zc:preprocessor from Visual Studio 2019 16.6 onward —
behaves consistently with GCC/Clang and supports
both the , ## extension and __VA_OPT__. This
flag should be standard in all new MSVC
projects:
cl
/Zc:preprocessor /std:c++latest ···Feature Detection
Feature detection macros allow code to adapt portably to different compilers, standard library versions, and available language features — without relying on version numbers that may not correlate with feature availability.
Header Availablity (C++17/C23)
The new __has_include tests whether a header file is
available without including it. Essential for writing code that works
across different standard library versions or provides fallback
implementations:
Portable header inclusion with fallback
#if __has_include(<format>)
# include <format>
# define HAS_STD_FORMAT 1
#else
# include "third_party/fmt/format.h" // fallback
# define HAS_STD_FORMAT 0
#endifOptionally including a non-essential header
#if __has_include(<version>)
# include <version> // C++20 feature-test macros without a full header
#endifThe __has_include accepts both angle-bracket and quoted
forms, matching #include syntax. It evaluates to
1 if the header is available, 0 if not.
Attribute Checks (C++20)
The __has_cpp_attribute tests whether a standard or
vendor attribute is supported:
Portable attribute macro
#if __has_cpp_attribute(nodiscard) >= 201907L
# define NODISCARD [[nodiscard]]
# define NODISCARD_R [[nodiscard("check the return value")]]
#else
# define NODISCARD
# define NODISCARD_R
#endif
NODISCARD int compute();
NODISCARD_R int open_file(const char* path);The value of __has_cpp_attribute is the year-month
integer of the paper that introduced the attribute — use
>= comparisons rather than == to be
forward-compatible.
Feature-Test Macros (C++20)
C++20 standardises a set of __cpp_* macros that allow
fine-grained, feature-level detection — more reliable than testing
__cplusplus alone, since not all compilers implement every
feature of a standard simultaneously.
Include <version> (C++20) to access these macros
without pulling in any other library components:
Checking for specific language and library features
#include <version>
#if __cpp_concepts >= 202002L
// use C++20 concepts
#elif defined __cpp_if_constexpr
// fall back to if constexpr
#else
// fall back to SFINAE
#endif
#if __cpp_lib_format >= 201907L
// std::format is available
#include <format>
#else
// use snprintf or a third-party library
#endif
#if __cpp_lib_ranges >= 201911L
#include <ranges>
#endifSelected commonly-used feature-test macros:
| Macro | Introduced | Tests for |
|---|---|---|
__cpp_concepts |
C++20 | Concepts language feature |
__cpp_if_constexpr |
C++17 | if constexpr |
__cpp_consteval |
C++20 | consteval keyword |
__cpp_lib_format |
C++20 | std::format |
__cpp_lib_ranges |
C++20 | Ranges library |
__cpp_lib_expected |
C++23 | std::expected |
__cpp_lib_print |
C++23 | std::print /
std::println |
__cpp_lib_flat_map |
C++23 | std::flat_map |
__cpp_lib_mdspan |
C++23 | std::mdspan |
A complete list of feature-test macros is maintained on cppreference.
Language Extensions
The preprocessor can be used to create new 'keywords' and superficial language extensions — macros that make C look like another language. Although this can be appealing, it is strongly discouraged: the result is code that other C/C++ programmers cannot read without consulting the macro definitions, and that defeats syntax highlighting, code analysis, and debuggers.
A brief illustration:
Example not to be followed
#define repeat do {
#define until(x) } while(!(x))
repeat
process();
until (done);The code compiles, but anyone reading it without the header is confused. Tools that analyse C syntax are confused. Do not write code like this.
The one pattern from this space that has genuine practical value is
the do/while(0) idiom (already covered), and
the UNUSED macro, which silences warnings about
deliberately unused parameters:
cpp — Unused parameters idiom
#define UNUSED(x) ((void)(x))
void callback(int event, void* userdata) {
UNUSED(userdata); // not needed in this handler
handle_event(event);
}Advanced Techniques
Be careful using ‘tricky’ or advanced preprocessor macros. They are not always easily understood and can create bugs. There are however some common patterns that all C/C++ programmers should recognise.
X-Macros
X-Macros are a technique for maintaining a single authoritative list of related data and generating multiple dependent code artefacts from it automatically. The list is defined once; every table, enum, function, or initialiser derived from it is generated by the preprocessor.
The fundamental idea: define a macro LIST_OF_THINGS that
expands to a series of invocations of another macro X,
where X is not yet defined. When you want to generate a
particular artefact, define X to produce one element of
that artefact, expand LIST_OF_THINGS, then
#undef X. Repeat with a different definition of
X for each artefact.
Definition of the list
#define COLOURS \
X(Red, "red", 255, 0, 0) \
X(Green, "green", 0, 255, 0) \
X(Blue, "blue", 0, 0, 255) \
X(Yellow, "yellow", 255, 255, 0)Each row is one element. The columns are: symbolic name, display string, R, G, B values. Now generate all the artefacts:
Generate an enum from the list
typedef enum Colour {
#define X(name, str, r, g, b) name,
COLOURS
#undef X
COLOUR_COUNT
} Colour;
// → Red=0, Green=1, Blue=2, Yellow=3, COLOUR_COUNT=4Generate a name lookup table
static const char* colour_name[] = {
#define X(name, str, r, g, b) str,
COLOURS
#undef X
};
// → {"red", "green", "blue", "yellow"}Generate an RGB lookup table
typedef struct { uint8_t r, g, b; } RGB;
static const RGB colour_rgb[] = {
#define X(name, str, r, g, b) {r, g, b},
COLOURS
#undef X
};Generate a print function
void print_colours(void) {
#define X(name, str, r, g, b) \
printf("%-8s rgb(%3d, %3d, %3d)\n", str, r, g, b);
COLOURS
#undef X
}The maintenance advantage is dramatic: adding
X(Cyan, "cyan", 0, 255, 255) to COLOURS
automatically propagates to the enum, both tables, and the print
function. No other source file needs to change.
X-Macros in a Header File
For larger projects, putting the list in a separate header file —
without an include guard, and with a conventional extension like
.def or .x — makes it a proper dependency that
build systems can track:
colours.def
— List of colours in a header
X(Red, "red", 255, 0, 0)
X(Green, "green", 0, 255, 0)
X(Blue, "blue", 0, 0, 255)
X(Yellow, "yellow", 255, 255, 0)
#undef X// Usage
typedef enum Colour {
#define X(name, str, r, g, b) name,
#include "colours.def"
COLOUR_COUNT
} Colour;The #undef X at the end of the file ensures the
definition is cleaned up after each inclusion, regardless of which
X was defined before the include.
The build system must list colours.def as a dependency
of every file that includes it, so that those files are recompiled
whenever the list changes. In CMake:
set_source_files_properties(main.c PROPERTIES OBJECT_DEPENDS colours.def)Resources on X-Macros
Wikipedia — X Macro
WikiBooks — C /
X-Macros
Embedded.com — X-Macros Part
1
Embedded.com — X-Macros Part
2
Embedded.com — X-Macros Part
3
Variadic Macro Iteration
A recurring need in C (and occasionally in C++) is applying a macro to each element of a variadic argument list. The preprocessor does not support recursion, so this is simulated with a fixed set of depth-named macros, one per supported argument count.
FOR_EACH
macro — supports up to 8 arguments
#define FE_1(M, x) M(x)
#define FE_2(M, x, ...) M(x) FE_1(M, __VA_ARGS__)
#define FE_3(M, x, ...) M(x) FE_2(M, __VA_ARGS__)
#define FE_4(M, x, ...) M(x) FE_3(M, __VA_ARGS__)
#define FE_5(M, x, ...) M(x) FE_4(M, __VA_ARGS__)
#define FE_6(M, x, ...) M(x) FE_5(M, __VA_ARGS__)
#define FE_7(M, x, ...) M(x) FE_6(M, __VA_ARGS__)
#define FE_8(M, x, ...) M(x) FE_7(M, __VA_ARGS__)
#define FE_GET_9TH(a1,a2,a3,a4,a5,a6,a7,a8,a9,...) a9
#define FOR_EACH(M, ...) \
FE_GET_9TH(__VA_ARGS__, \
FE_8,FE_7,FE_6,FE_5,FE_4,FE_3,FE_2,FE_1,~)\
(M, __VA_ARGS__)The trick: FE_GET_9TH shifts the argument list by the
number of variadic arguments, landing on the correctly-named
FE_N macro. The outer FOR_EACH macro then
calls that FE_N with M and the original
argument list.
Using
FOR_EACH to forward-declare a list of handlers
#define DECLARE_HANDLER(name) void handle_##name(Request* req);
FOR_EACH(DECLARE_HANDLER, login, logout, search, upload, download)Expands to:
void handle_login(Request* req);
void handle_logout(Request* req);
void handle_search(Request* req);
void handle_upload(Request* req);
void handle_download(Request* req);Using
FOR_EACH to populate a dispatch table
typedef void (*Handler)(Request*);
static const Handler dispatch_table[] = {
#define ENTRY(name) handle_##name,
FOR_EACH(ENTRY, login, logout, search, upload, download)
#undef ENTRY
};This technique is particularly useful at C API boundaries, for
registration patterns, and in embedded systems where the set of
registered items must be determined at compile time. For most C++
applications, std::tuple, fold expressions, or template
parameter packs are a better fit — but FOR_EACH remains
practical in mixed-language codebases and C-compatible interfaces.
C++ Using Directives
C++ allows multiple identifiers in a single
using-declaration, which has been valid since C++11 and was
clarified in C++17:
using std::cin, std::cout, std::string, std::vector; // valid C++11/17Where the FOR_EACH technique is still useful in
C++ is in generating using declarations programmatically.
For example, in a template library that exposes a configurable set of
names, or in a code generation context where the list of identifiers is
data-driven:
Programmatic
using declarations with FOR_EACH
#define USING_(ns, id) using ns id;
#define USING(ns, ...) FOR_EACH(USING_##ns, __VA_ARGS__)This is a demonstration of the technique, not a practical
recommendation for everyday C++ code. Use
using std::a, b, c; directly.
Preprocessor in Modern C++
From C++11 onward, most of the preprocessor's historical roles in C++ have been superseded by language features. Understanding which tasks still require the preprocessor — and which do not — helps you write cleaner, safer, more maintainable code.
| Old Pattern | Modern C++ Replacement | Needed? |
|---|---|---|
#define PI 3.14159 |
constexpr double PI = 3.14159; |
No |
#define MAX_N 256 |
constexpr int max_n = 256; |
No |
#define SQR(x) ((x)*(x)) |
template<typename T> constexpr T sqr(T x) |
No |
#define MAX(a,b) ... |
std::max(a, b) |
No |
assert() |
static_assert
(compile-time) |
Yes |
#ifdef type selection |
if constexpr, concepts |
No |
#include "header.h" |
import mymodule; (C++20
modules) |
Yes |
| Platform/compiler detection | (nothing) | Yes |
| Conditional header inclusion | __has_include |
Yes |
| Build feature flags | (nothing) | Yes |
C linkage guard
#extern "C" |
#if defined __cplusplus |
Yes |
| Warning suppression | _Pragma,
#pragma |
Yes |
| X-Macro code generation | (nothing equivalent) | Yes (C) |
What the preprocessor still uniquely owns in C++:
Platform, OS, and compiler detection —
#ifdef __linux__,#ifdef _MSC_VER,#ifdef __arm__. No language feature can query these.Conditional header inclusion —
__has_includeand#ifare the only way to optionally include a header.Feature-test macros —
__cpp_*,__has_cpp_attribute. Essential for portable, forward-compatible library code.Build-time configuration —
-DNDEBUG,-DENABLE_FEATURE=1. Flags from the build system must enter the translation unit through macros.C Linkgage (
extern "C") — the canonical pattern for headers shared between C and C++ requires#if defined __cplusplus.Include guards /
#pragma once— C++20 modules will eventually supersede this, but header-based code will be with us for a long time.Warning suppression around third-party code —
#pragma GCC diagnostic push/popand equivalents.File & Line location —
__FILE__/__LINE__in macros** when the call-site location must be captured in a macro (not in a function), only the preprocessor can do it.X-Macros — data-driven code generation with no runtime overhead and no language-level equivalent, especially important in C.
The goal is not to eliminate the preprocessor — it is to use it only where the language provides no better alternative.
Summary
Non-Recursive Macro Expansion
Macros are expanded recursively in the sense that the expansion is
examined for further macros — but the originating macro is never
re-expanded. This is not a limitation but a deliberate design: it
prevents infinite recursion. The following expands to MACRO
and stops:
#define MACRO MACRO
MACROMacro Pitfalls — Quick Reference
| Pitfall | Example | Fix |
|---|---|---|
| Missing parameter parens | #define SQR(x) x*x |
((x)*(x)) |
| Missing expansion parens | #define NEG(x) -x |
(-(x)) |
| Double evaluation | SQR(++i) |
Use inline function |
| Multi-statement without guard | #define F() a();b() |
Wrap in do{...}while(0) |
Side effects in assert |
assert(connect()) |
Never — no-op in release |
| Trailing comma in variadic | LOG(fmt) with
__VA_ARGS__ |
Use __VA_OPT__ (C++20) |
| Token paste prevents expand | #define F(a,b) a##b |
Add indirection level |
| Stringification not expanded | STRINGIFY(MACRO) →
"MACRO" |
Use double indirection |
| Macro in string literal | "__FILE__" |
Macros not expanded in strings |
Conditional Compilation
This is so useful that even C# has adopted it. By changing the compilation command line, Makefile rules, or IDE configuration, different parts of a program can be compiled in or out. The most common application is debug-only code:
#ifndef NDEBUG
validate_state();
#endifNDEBUG is the only standard way to distinguish
debug from release compilation. When NDEBUG is defined,
assert() expands to nothing. Define it in release builds
with -DNDEBUG on the command line.
The Preprocessor and C++
The preprocessor is shared between C and C++. From C++11 onward, its role in C++ has narrowed substantially:
- Use
constexprfor constants — typed, scoped, debugger-visible. - Use
inlinetemplate functions for generic "fast" functions. - Use
static_assertfor compile-time invariant checks. - Use
if constexprfor type-conditional code in templates. - Use the
[[nodiscard]],[[deprecated]],[[likely]]attributes instead of macro-based equivalents.
The preprocessor's indispensable remaining roles in C++ are: file
inclusion, conditional compilation for platform detection, build feature
flags, extern "C" guards, #pragma once, and
warning suppression.
In C, macros with parameters remain essential — the language has no
templates, no constexpr, and no inline
generics. Use them carefully, document their pitfalls, and always
parenthesise.
Some Resources
GNU C Preprocessor Manual
cppreference —
Preprocessor
cppreference — Feature test
macros
cpredef — Predefined
macros
Boost Preprocessor
Library
IAR — Preprocessor
Tips and Tricks
Wikipedia — X Macro
WikiBooks — C /
X-Macros
StackOverflow —
FOR_EACH Macro
GitHub
— pfultz2/Cloak
- 2026-06-06: Major rewrite. Corrected factual errors. Update standards.
- 2018-07-09: Created. Modified from previous Course Notes.