Preprocessor

C/C++ Preprocessor Features

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:

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: -Dmacro›, or: -Dmacro=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 definedmacro›.
#ifndef True if the named macro is not defined. Equivalent to #if !definedmacro›.
#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.

NOTEConditional 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).

SyntaxMacro Directive

  • #define macro
    Defines macro with an empty expansion. Same as -Dmacro on the command line.
  • #define macro 1
    Defines macro expanding to 1. Same as -Dmacro=1 on the command line.
  • #define macro expansion
    Defines macro expanding to expansion.
  • #define macro(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
X

This 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 pasted

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

TIPMacro 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.c

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

NoteOptional 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, UB

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

SyntaxInclude 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_H

The 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
// ...
#endif

UUIDs 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

#endif

Generate 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
}
#endif

TIPListing 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"
#endif

Conditionals Comparision

#ifdefmacro›’ is exactly equivalent to ‘#if definedmacro›’, and #ifndefmacro›’ to ‘#if !definedmacro›’. 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
#endif

Use #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 → false

This 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 line

C++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();
#endif
With C++23/C23 shortcuts
#ifdef PLATFORM_LINUX
   use_epoll();
#elifdef PLATFORM_MACOS
   use_kqueue();
#elifndef PLATFORM_WINDOWS
   #error "Unknown platform"
#else
   use_iocp();
#endif

C++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);
#endif

The 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 #pragmaargs›. Standardised in C99 / C++11. The only standard way to emit a pragma from within a macro expansion, since macros cannot otherwise generate directives.

NOTECharisation 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 this

The 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
#endif

Stringify

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'
#endif

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

NOTEStringification 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 A

This 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 #pragmastring›, 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_WARNING

Note 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:

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 pop
Warning 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 header

Although 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:

cppC++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.

cppC++ 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 bug

A 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)
#endif

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

cppStatic 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 error

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

NOTEMSVC 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
#endif
Optionally including a non-essential header
#if __has_include(<version>)
#  include <version>   // C++20 feature-test macros without a full header
#endif

The __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>
#endif

Selected 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:

cppUnused 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=4
Generate 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.defList 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/17

Where 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++:

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
MACRO

Macro 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();
#endif

NDEBUG 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:

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.