Skip to content
C++
Language
Basic

Preprocessor

C++ preprocessor — macros, include guards, conditional compilation,

Preprocessorsince C++98

The C++ preprocessor is a text-substitution phase that runs before compilation, handling #include file insertion, #define macro expansion, and #if/#ifdef conditional compilation — operating entirely on tokens, with no knowledge of types, scopes, or overload resolution.

Overview

The preprocessor transforms a source file into a single translation unit through a fixed pipeline:

  1. Splice lines ending with \ (physical → logical lines)
  2. Tokenise into preprocessing tokens
  3. Expand #include directives recursively
  4. Expand macros
  5. Evaluate #if/#elif/#else/#endif branches
  6. Concatenate adjacent string literals

Because it works below the type system, macros are hard to debug, hard to scope, and easy to misuse. The bulk of historical macro usage — constants, inline functions, compile-time assertions — has better replacements in modern C++. A handful of uses remain genuinely irreplaceable.


Include Guards and #pragma once

Every header must prevent double-inclusion.

cpp
// Traditional include guard — portable, mandated by the standard
#ifndef MYLIB_WIDGET_HPP
#define MYLIB_WIDGET_HPP

// ... header content ...

#endif  // MYLIB_WIDGET_HPP
cpp
// #pragma once — non-standard but supported by GCC, Clang, MSVC, and
// every other mainstream compiler. Simpler and sidesteps name collisions.
#pragma once

// ... header content ...

#pragma once is often faster to compile (the preprocessor can skip re-opening the file). The standard does not mandate it, but it is universally supported in practice. For headers targeting embedded toolchains or non-mainstream compilers, use the traditional guard.

Since C++17, __has_include lets you probe for header availability:

cpp
#if __has_include(<version>)          // C++20 feature-test umbrella header
    #include <version>
#endif

#if __has_include(<optional>)         // C++17
    #include <optional>
    #define HAS_STD_OPTIONAL 1
#elif __has_include(<experimental/optional>)
    #include <experimental/optional>
    namespace std { using optional = std::experimental::optional; }
    #define HAS_STD_OPTIONAL 1
#else
    #define HAS_STD_OPTIONAL 0
#endif

Object-like and Function-like Macros

Object-like macros

cpp
// BAD: no type, no scope — TIMEOUT_MS leaks globally out of any namespace
namespace net {
    #define TIMEOUT_MS 5000
}

// GOOD (C++11): typed, scoped, debuggable
namespace net {
    constexpr int kTimeoutMs = 5000;
}

// NULL is still defined in <cstddef> for C compatibility;
// use nullptr in C++ code (C++11)

Function-like macros

cpp
// BAD: double evaluation, no type safety
#define MAX(a, b) ((a) > (b) ? (a) : (b))
int r = MAX(i++, j++);  // i or j incremented twice — undefined behaviour

// GOOD (C++11): zero-overhead, type-safe, debuggable
template<typename T>
constexpr T max_of(T a, T b) noexcept { return a > b ? a : b; }

// Macros remain necessary for a few things constexpr cannot do:
#define STRINGIFY(x)   #x
#define CONCAT(a, b)   a##b
#define ARRAY_SIZE(a)  (sizeof(a) / sizeof((a)[0]))  // prefer std::size() in C++17

// Logging — captures __FILE__/__LINE__ at the call site, not inside the logger
#define LOG_ERROR(msg) \
    ::log_impl(::LogLevel::Error, __FILE__, __LINE__, (msg))

Stringification and Token Pasting

# converts the following preprocessing token into a string literal. ## concatenates two tokens before re-scanning.

cpp
#define STRINGIFY(x)  #x
#define XSTRINGIFY(x) STRINGIFY(x)   // forces expansion of x before stringifying

const char* a = STRINGIFY(hello);    // "hello"
const char* b = STRINGIFY(1 + 2);    // "1 + 2"

#define VERSION 42
const char* c = STRINGIFY(VERSION);  // "VERSION" — no expansion
const char* d = XSTRINGIFY(VERSION); // "42"       — expanded first

// Token pasting: ## joins tokens, useful for generated field/variable names
#define FIELD(type, name) type m_##name
struct Point {
    FIELD(double, x);   // double m_x;
    FIELD(double, y);   // double m_y;
};

// static_assert with stringified condition gives readable error messages (C++11)
#define STATIC_REQUIRE(expr) \
    static_assert((expr), "Requirement failed: " #expr)

STATIC_REQUIRE(sizeof(long) == 8);
// error: Requirement failed: sizeof(long) == 8   (on a 4-byte-long platform)

Conditional Compilation

cpp
// __cplusplus values:
//   199711L = C++98/03  |  201103L = C++11  |  201402L = C++14
//   201703L = C++17     |  202002L = C++20  |  202302L = C++23
#if __cplusplus >= 202002L
    #include <concepts>
    #define HAS_CONCEPTS 1
#else
    #define HAS_CONCEPTS 0
#endif

// Platform detection
#if defined(_WIN32)
    #define PLATFORM_WINDOWS
    #include <windows.h>
#elif defined(__APPLE__)
    #define PLATFORM_MACOS
    #include <unistd.h>
#elif defined(__linux__)
    #define PLATFORM_LINUX
    #include <unistd.h>
#endif

// Debug vs. release — NDEBUG is set by CMake in Release builds
#ifdef NDEBUG
    #define ASSERT(expr) ((void)0)
#else
    #define ASSERT(expr) \
        do { \
            if (!(expr)) { \
                std::fprintf(stderr, "Assert failed: %s at %s:%d\n", \
                             #expr, __FILE__, __LINE__); \
                std::abort(); \
            } \
        } while (false)
#endif

// C++17: feature-test macros from <version> (or any standard header)
#ifdef __cpp_lib_filesystem          // C++17
    #include <filesystem>
    namespace fs = std::filesystem;
#endif

// C++20: __has_cpp_attribute — safer than bare #ifdef for attributes
#if __has_cpp_attribute(nodiscard)   // C++20
    #define NODISCARD [[nodiscard]]
#else
    #define NODISCARD
#endif

Predefined Macros

cpp
// Standard — guaranteed by every conforming implementation
__cplusplus         // standard version (see above)
__FILE__            // string literal: current source file path
__LINE__            // integer constant: current line number
__DATE__            // "Mmm dd yyyy" — compilation date
__TIME__            // "hh:mm:ss"   — compilation time
__STDC_HOSTED__     // 1 for hosted (OS present), 0 for freestanding

// __func__ is NOT a preprocessor macro — it is a predefined identifier (C++11)
// defined as a static const char[] local to each function body.
void foo() {
    std::puts(__func__);   // "foo"
}

// Compiler-specific
__GNUC__            // GCC major version (also set by Clang for compatibility)
__GNUC_MINOR__      // GCC minor version
__clang__           // Clang (also defines __GNUC__)
__clang_major__     // Clang major version
_MSC_VER            // MSVC: 1900=VS2015, 1910+=VS2017, 1920+=VS2019, 1930+=VS2022
__MINGW32__         // MinGW 32-bit
__MINGW64__         // MinGW 64-bit

// Architecture
__x86_64__          // x86-64 (GCC/Clang)
_M_X64              // x86-64 (MSVC)
__aarch64__         // ARM 64-bit (GCC/Clang)
_M_ARM64            // ARM 64-bit (MSVC)
__ARM_ARCH          // ARM architecture version number

X Macros — Table-Driven Code Generation

X macros eliminate the need to keep parallel enums, string tables, and dispatch functions in sync. Define the data once; instantiate multiple times:

cpp
// Single source of truth: X(enum_name, display_string, http_code)
#define HTTP_ERRORS(X) \
    X(BadRequest,          "Bad Request",           400) \
    X(Unauthorized,        "Unauthorized",          401) \
    X(Forbidden,           "Forbidden",             403) \
    X(NotFound,            "Not Found",             404) \
    X(InternalServerError, "Internal Server Error", 500)

// 1. Enum generation
enum class HttpError {
#define X(name, str, code) name,
    HTTP_ERRORS(X)
#undef X
    _Count
};

// 2. Integer code lookup
int http_code(HttpError e) {
    switch (e) {
#define X(name, str, code) case HttpError::name: return code;
        HTTP_ERRORS(X)
#undef X
    }
    return -1;
}

// 3. Human-readable string
const char* http_message(HttpError e) {
    switch (e) {
#define X(name, str, code) case HttpError::name: return str;
        HTTP_ERRORS(X)
#undef X
    }
    return "Unknown";
}
// Adding a new error requires exactly one line in HTTP_ERRORS.
// Every derived table updates automatically; forgetting one is impossible.

The #undef X after each instantiation block is mandatory — without it the local helper macro leaks into subsequent code.


Variadic Macros

__VA_ARGS__ was standardised in C++11 (it existed in C99 but was absent from C++98/03).

cpp
// C++11: basic variadic forwarding
#define TRACE(fmt, ...) std::printf("[TRACE] " fmt "\n", __VA_ARGS__)
TRACE("pid=%d port=%d", pid, port);

The classic problem: TRACE("hello") with no variadic arguments produces a trailing comma before __VA_ARGS__, which is ill-formed. GCC and Clang accept ##__VA_ARGS__ as an extension. C++20 standardised __VA_OPT__:

cpp
// __VA_OPT__(tokens) expands to tokens only when __VA_ARGS__ is non-empty — C++20
#define LOG(fmt, ...) ::log_write(fmt __VA_OPT__(,) __VA_ARGS__)

LOG("startup complete");           // log_write("startup complete")
LOG("code=%d msg=%s", code, msg);  // log_write("code=%d msg=%s", code, msg)

#pragma

#pragma issues implementation-defined directives. Unrecognised pragmas are silently ignored — this is by design, enabling portable use of compiler-specific pragmas.

cpp
#pragma once   // include guard — near-universal, non-standard

// GCC / Clang: diagnostic control
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
    LegacyApi::old_call();
#pragma GCC diagnostic pop

// MSVC
#pragma warning(push)
#pragma warning(disable: 4100)   // C4100: unreferenced formal parameter
    void stub(int) {}
#pragma warning(pop)

// Struct packing — removes padding for wire protocols and file formats
#pragma pack(push, 1)
struct WireHeader {
    uint8_t  version;    // offset 0
    uint16_t length;     // offset 1 (potentially unaligned — caution on ARM)
    uint32_t checksum;   // offset 3
};  // sizeof == 7, not 8
#pragma pack(pop)

// GCC / Clang: per-TU optimisation level (overrides -O flag for this file)
#pragma GCC optimize("O3", "unroll-loops")
#pragma GCC target("avx2")

Best Practices

AvoidPrefer
#define CONST 42constexpr int kConst = 42; (C++11)
#define MAX(a,b) ...template<typename T> constexpr T max(T,T) (C++11)
#ifdef DEBUG logic branchesif constexpr (kDebug) (C++17) — always type-checked
#define ASSERT(x) roll-your-ownstatic_assert for compile-time (C++11); assert() for runtime
NULLnullptr (C++11)
ARRAY_SIZE(a) macrostd::size(a) (C++17) or std::ssize(a) (C++20)
#define NODISCARD [[nodiscard]]#if __has_cpp_attribute(nodiscard) guard (C++20)

Use a project-wide prefix (MYPROJECT_) on all macro names. Common words (MIN, ERROR, TRUE) collide with system headers and third-party code. Macros ignore namespaces entirely — a #define in a .cpp file is global for the remainder of that translation unit.

Wrap multi-statement macros in do { } while (false) so they require a trailing semicolon and behave correctly inside braceless if/else.


Common Pitfalls

Double evaluation:

cpp
#define SQUARE(x) ((x) * (x))
int n = 3;
int bad = SQUARE(n++);   // (n++) * (n++): undefined behaviour
// Fix: constexpr or inline function — arguments evaluate exactly once

Missing parentheses around arguments or result:

cpp
#define DOUBLE(x) x + x
int r = 10 * DOUBLE(3);   // 10 * 3 + 3 = 33, not 60
// Fix: #define DOUBLE(x) ((x) + (x))

Stringification without the double-expansion trick:

cpp
#define VERSION 5
#define DIRECT(x)   #x
#define INDIRECT(x) DIRECT(x)

const char* a = DIRECT(VERSION);    // "VERSION" — macro not expanded
const char* b = INDIRECT(VERSION);  // "5"        — expanded first

#ifdef-gated code that rots silently:
Code inside #ifdef PLATFORM_WINDOWS is not compiled on Linux CI. Type errors and API changes accumulate undetected. Prefer if constexpr (C++17) for logic that should remain visible to the compiler on all platforms, using constexpr bool kWindows = ... flags.

Include guard name collisions:
Two headers with the same guard symbol in different directories silently cause one to be skipped entirely. A unique prefix — ACME_NET_HTTP_CLIENT_HPP rather than HTTP_CLIENT_HPP — prevents this.


See Also

  • constexpr — compile-time constants and functions; replaces most object/function macros
  • Concepts — C++20 constraints that replace static_assert-based SFINAE and STATIC_CHECK macros
  • Attributes[[nodiscard]], [[deprecated]], and others that replace common warning-suppression macros
  • Modules — C++20 alternative to #include and header guards