Preprocessor and Macros

This section documents C23 (ISO/IEC 9899:2024), per ISO/IEC JTC1/SC22/WG14’s freely available working draft N3220, which WG14 documents as differing from the published standard only editorially — the reference these pages are written and verified against.

This content was generated with the assistance of AI and should be verified against the WG14 draft and cppreference.com’s C reference before being relied on in production.

This section’s bibliography lists the reference material consulted while preparing these pages.

The preprocessor is a text processor that runs before the compiler sees anything (translation phases 1-6, see Program Structure). It knows nothing about types, scopes or statements — which makes it powerful, and makes every macro a potential surprise.

The modern guidance is simple: use #include, include guards and conditional compilation freely; prefer constexpr, enum, static inline and _Generic to macros for anything the compiler can express; and when you do write a macro, follow the hygiene rules below.

Object-Like Macros

#include <stdio.h>

#define BUFFER_SIZE 4096                // a token sequence, not a typed constant
#define GREETING "hello"
#define EMPTY                           // expands to nothing

int main(void)
{
    char buffer[BUFFER_SIZE];
    printf("%s %zu\n", GREETING, sizeof buffer);
    EMPTY
    return 0;
}

An object-like macro is pure text substitution. It has no type, obeys no scope, and does not appear in the debugger — which is why an enum constant or a C23 constexpr object is preferable whenever the value is only needed by the compiler. Reserve macros for what the preprocessor itself needs (#if conditions, string literals to concatenate, header configuration).

Function-Like Macros and Hygiene

#include <stdio.h>

// Rule 1: parenthesize EVERY parameter and the whole body.
#define SQUARE(x) ((x) * (x))

// Without those parentheses:
#define BAD_SQUARE(x) x * x            // BAD_SQUARE(1 + 2) is 1 + 2 * 1 + 2 == 5

// Rule 2: never use a parameter twice -- the argument's side effects repeat.
#define MAX_UNSAFE(a, b) ((a) > (b) ? (a) : (b))

// Rule 3: wrap multi-statement macros in do { ... } while (0) so they behave
// like a single statement in every context, including an unbraced if/else.
#define LOG_AND_ADD(sum, value)     \
    do {                            \
        printf("adding %d\n", (value)); \
        (sum) += (value);           \
    } while (0)

int main(void)
{
    printf("%d %d\n", SQUARE(1 + 2), BAD_SQUARE(1 + 2));    // 9 5

    int i = 3;
    printf("%d\n", MAX_UNSAFE(i++, 2));     // i is incremented TWICE -- avoid
    printf("i = %d\n", i);

    int sum = 0;
    if (sum == 0)
        LOG_AND_ADD(sum, 5);                // works even unbraced, thanks to do/while(0)
    else
        LOG_AND_ADD(sum, 7);

    printf("sum = %d\n", sum);
    return 0;
}

The double-evaluation problem has no macro-level fix in standard C — which is the strongest argument for static inline functions:

#include <stdio.h>

// A function evaluates each argument exactly once, is typed, and is just as fast at -O2.
static inline int max_int(int a, int b)
{
    return a > b ? a : b;
}

int main(void)
{
    int i = 3;
    int result = max_int(i++, 2);       // the argument is evaluated exactly once
    printf("%d, i = %d\n", result, i);  // 3, i = 4

    // Note the separate statement: putting max_int(i++, 2) and i in one printf
    // would be an unsequenced access to i, and undefined regardless of the callee.
    return 0;
}

Naming convention: UPPER_SNAKE_CASE for macros, so a reader can see that ordinary evaluation rules may not apply.

#include and Include Guards

// #include <stdio.h>      -- angle brackets: the implementation's include path
// #include "project.h"    -- quotes: the current file's directory first, then the path

Every header must be idempotent, because it will be included more than once:

widget.h
#ifndef PROJECT_WIDGET_H            // a unique name -- prefix it with the project
#define PROJECT_WIDGET_H

typedef struct Widget Widget;       // opaque handle

Widget *widget_create(void);
void widget_destroy(Widget *w);

#endif /* PROJECT_WIDGET_H */

#pragma once does the same thing in one line and is supported by GCC, Clang and MSVC, but it is not standard C and can misbehave when a header is reachable through symlinks or multiple mount paths. Include guards remain the portable choice; many projects use both.

Conditional Compilation

#include <stdio.h>

#define FEATURE_LEVEL 2

int main(void)
{
#if FEATURE_LEVEL >= 3
    puts("full");
#elif FEATURE_LEVEL == 2
    puts("standard");               // this one is compiled
#else
    puts("minimal");
#endif

#ifdef FEATURE_LEVEL                // "is it defined at all?"
    puts("feature level is set");
#endif

#ifndef DISABLE_LOGGING
    puts("logging on");
#endif

    // C23 shorthands for #ifdef/#ifndef inside an #if chain:
#if 0
    puts("never");
#elifdef FEATURE_LEVEL              // C23
    puts("elifdef taken");
#elifndef SOMETHING_ELSE            // C23
    puts("not reached");
#endif

    return 0;
}

Notes that matter in real headers:

  • defined(X) works inside a larger #if expression: #if defined(A) && !defined(B).

  • An undefined identifier in an #if expression evaluates to 0 — so a typo in a macro name silently takes the false branch. -Wundef turns that into a warning.

  • #if arithmetic uses the widest integer types and cannot use sizeof, casts, floating-point or enum constants.

  • The #if 0 … #endif block is the correct way to comment out a region of code, since block comments do not nest.

Portability Guards

#include <stdio.h>

// Compiler detection, in the order that avoids false positives:
#if defined(__clang__)
#  define COMPILER_NAME "clang"
#elif defined(__GNUC__)
#  define COMPILER_NAME "gcc"
#elif defined(_MSC_VER)
#  define COMPILER_NAME "msvc"
#else
#  define COMPILER_NAME "unknown"
#endif

// Language-version detection: the ONLY way to tell C17 from C23.
#if defined(__STDC_VERSION__) && __STDC_VERSION__ >= 202311L
#  define C_EDITION "C23"
#elif defined(__STDC_VERSION__) && __STDC_VERSION__ >= 201710L
#  define C_EDITION "C17"
#elif defined(__STDC_VERSION__) && __STDC_VERSION__ >= 201112L
#  define C_EDITION "C11"
#else
#  define C_EDITION "C99 or earlier"
#endif

// Platform detection.
#if defined(_WIN32)
#  define PLATFORM_NAME "windows"
#elif defined(__linux__)
#  define PLATFORM_NAME "linux"
#elif defined(__APPLE__)
#  define PLATFORM_NAME "macos"
#else
#  define PLATFORM_NAME "other"
#endif

int main(void)
{
    printf("%s / %s / %s\n", COMPILER_NAME, C_EDITION, PLATFORM_NAME);
    return 0;
}

C23 makes two of these checks first-class:

#include <stdio.h>

// __has_include (C23; a GCC/Clang extension long before): probe for a header.
#if defined(__has_include)
#  if __has_include(<stdbit.h>)
#    include <stdbit.h>
#    define HAVE_STDBIT 1
#  else
#    define HAVE_STDBIT 0
#  endif
#else
#  define HAVE_STDBIT 0
#endif

// __has_c_attribute (C23): probe for an attribute before using it.
#if defined(__has_c_attribute) && __has_c_attribute(nodiscard)
#  define MUST_CHECK [[nodiscard]]
#else
#  define MUST_CHECK
#endif

MUST_CHECK static int compute(void)
{
    return 42;
}

int main(void)
{
    printf("stdbit: %d, value: %d\n", HAVE_STDBIT, compute());
    return 0;
}

Stringification and Token Pasting

#include <stdio.h>

// # turns an argument into a string literal; the two-level indirection is needed
// so that the argument is macro-expanded first.
#define STRINGIFY_RAW(x) #x
#define STRINGIFY(x) STRINGIFY_RAW(x)

// ## pastes two tokens into one identifier.
#define CONCAT_RAW(a, b) a##b
#define CONCAT(a, b) CONCAT_RAW(a, b)

#define VERSION 23

// A generated pair of functions -- the "X macro" family of tricks.
#define DEFINE_GETTER(type, name)           \
    static type CONCAT(get_, name)(void)    \
    {                                       \
        return (type)0;                     \
    }

DEFINE_GETTER(int, count)
DEFINE_GETTER(double, ratio)

int main(void)
{
    printf("%s %s\n", STRINGIFY_RAW(VERSION), STRINGIFY(VERSION));   // "VERSION" "23"
    printf("%d %g\n", get_count(), get_ratio());
    return 0;
}

The one-argument/two-argument dance is the classic gotcha: and # suppress expansion of their operands, so a wrapper macro is needed to expand first.

Variadic Macros and __VA_OPT__

#include <stdio.h>

// C99: at least one argument must follow the named ones.
#define LOG_C99(format, ...) fprintf(stderr, format, __VA_ARGS__)

// The GNU workaround for zero variadic arguments -- non-standard:
// #define LOG_GNU(format, ...) fprintf(stderr, format, ##__VA_ARGS__)

// C23: __VA_OPT__(x) expands to x only when __VA_ARGS__ is non-empty,
// so this works with zero extra arguments and is fully standard.
#define LOG(format, ...) \
    fprintf(stderr, "[%s:%d] " format "\n", __FILE__, __LINE__ __VA_OPT__(,) __VA_ARGS__)

// The leading 0 only keeps the compound literal non-empty, so it is not counted.
#define COUNT_ARGS(...) COUNT_ARGS_IMPL(0 __VA_OPT__(,) __VA_ARGS__)
#define COUNT_ARGS_IMPL(...) (sizeof (int[]){ __VA_ARGS__ } / sizeof(int) - 1)

int main(void)
{
    LOG("starting");                            // zero variadic arguments -- fine in C23
    LOG("value = %d", 42);
    LOG("pair = %d,%d", 1, 2);
    LOG_C99("%s\n", "explicit argument");

    printf("%zu %zu\n", COUNT_ARGS(), COUNT_ARGS(1, 2, 3));     // 0 3
    return 0;
}

Predefined Macros

#include <stdio.h>

int main(void)
{
    printf("file: %s\n", __FILE__);                 // the source file name
    printf("line: %d\n", __LINE__);                 // the current line number
    printf("function: %s\n", __func__);             // C99 -- an identifier, not a macro
    printf("date/time: %s %s\n", __DATE__, __TIME__);
    printf("standard C: %d\n", __STDC__);
    printf("version: %ld\n", __STDC_VERSION__);     // 202311L for C23

#ifdef __STDC_NO_THREADS__
    puts("no <threads.h> on this implementation");
#endif
#ifdef __STDC_NO_VLA__
    puts("no variable-length arrays");
#endif
#ifdef __STDC_NO_ATOMICS__
    puts("no <stdatomic.h>");
#endif

    return 0;
}

__func__ is technically not a macro but a predefined identifier holding the enclosing function’s name — which makes the standard logging macro:

#include <stdio.h>

#define TRACE(...) \
    do { \
        fprintf(stderr, "%s:%d:%s: ", __FILE__, __LINE__, __func__); \
        fprintf(stderr, __VA_ARGS__); \
        fputc('\n', stderr); \
    } while (0)

static void work(int units)
{
    TRACE("processing %d units", units);
}

int main(void)
{
    work(3);
    return 0;
}

#error, #warning and _Pragma

#include <limits.h>
#include <stdio.h>

// Refuse to compile on an unsupported configuration, with a readable message.
#if CHAR_BIT != 8
#  error "this code requires 8-bit bytes"
#endif

#if !defined(__STDC_VERSION__) || __STDC_VERSION__ < 201112L
#  error "this code requires C11 or later"
#endif

// #warning (C23; a long-standing extension before that) notes without failing:
#if 0
#  warning "legacy path compiled -- migrate to the new API"
#endif

int main(void)
{
    // _Pragma is the operator form of #pragma, so it can be used inside a macro:
#define DIAGNOSTIC_PUSH  _Pragma("GCC diagnostic push")
#define IGNORE_UNUSED    _Pragma("GCC diagnostic ignored \"-Wunused-variable\"")
#define DIAGNOSTIC_POP   _Pragma("GCC diagnostic pop")

    DIAGNOSTIC_PUSH
    IGNORE_UNUSED
    int deliberately_unused = 1;
    DIAGNOSTIC_POP

    puts("compiled");
    return 0;
}

#error is the right way to fail fast on a bad configuration — far better than compiling and misbehaving.

#embed (C23)

#embed inserts the contents of a binary file as a comma-separated list of byte values — no build-time conversion script, no xxd -i:

#include <stdio.h>

// The file's bytes become an initializer list.
static const unsigned char icon[] = {
#embed "icon.png"
};

// if_empty, limit, prefix and suffix are the standard parameters:
static const unsigned char header[] = {
#embed "icon.png" limit(8)
};

int main(void)
{
    printf("%zu bytes, first = %02X\n", sizeof icon, header[0]);
    return 0;
}

#embed requires GCC 15 or later or Clang 19 or later; the Clang 18 used to verify the examples in this section does not implement it, so the block above is written to the standard rather than compile-checked. Until your baseline supports it, generate a C array from the binary at build time (xxd -i, ld -r -b binary, or a CMake custom command).

Argument Checking and Default Arguments

C has no default arguments, but the preprocessor can fake them — and _Generic does it better:

#include <stdio.h>

// Count-based dispatch: pick a macro by the number of arguments.
#define GET_MACRO(_1, _2, _3, NAME, ...) NAME
#define CONNECT(...) GET_MACRO(__VA_ARGS__, CONNECT3, CONNECT2, CONNECT1)(__VA_ARGS__)

#define CONNECT1(host)                connect_full((host), 80, 30)
#define CONNECT2(host, port)          connect_full((host), (port), 30)
#define CONNECT3(host, port, timeout) connect_full((host), (port), (timeout))

static int connect_full(const char *host, int port, int timeout)
{
    printf("connect %s:%d timeout=%d\n", host, port, timeout);
    return 0;
}

int main(void)
{
    CONNECT("example.com");
    CONNECT("example.com", 8080);
    CONNECT("example.com", 8080, 5);
    return 0;
}

This works, and it is exactly the kind of cleverness to use sparingly: the error messages are terrible and the macro cannot be stepped through. A plain connect_full plus a small wrapper function is usually the better engineering choice. For type-based rather than count-based dispatch, see Type-Generic Programming.

See Also

References