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.


Feature Detection with __has_include and __has_cpp_attribute

Both intrinsics let you probe the environment at preprocessing time without relying on fragile version numbers. __has_include (C++17) tests whether a header exists in the implementation's search path; __has_cpp_attribute (C++20) tests whether an attribute is supported and returns its feature-test value.

cpp
// C++17
// __has_include: probe for headers before including them
#if __has_include(<filesystem>)
    #include <filesystem>
    namespace fs = std::filesystem;
#elif __has_include(<experimental/filesystem>)
    #include <experimental/filesystem>
    namespace fs = std::experimental::filesystem;
#else
    #error "No filesystem header found — C++17 or GCC 6+ required"
#endif

// Detecting third-party or platform-specific headers
#if __has_include(<zlib.h>)
    #include <zlib.h>
    #define HAS_ZLIB 1
#else
    #define HAS_ZLIB 0
#endif

__has_cpp_attribute returns the year-month integer from the SD-6 feature-test table, or 0 if the attribute is not supported. Use it to write attribute wrappers that degrade gracefully on older compilers.

cpp
// C++20
// __has_cpp_attribute: safe wrapper for attributes across compiler versions
#if __has_cpp_attribute(nodiscard) >= 201907L   // nodiscard(reason) since C++20
    #define NODISCARD(msg) [[nodiscard(msg)]]
#elif __has_cpp_attribute(nodiscard)
    #define NODISCARD(msg) [[nodiscard]]
#else
    #define NODISCARD(msg)
#endif

#if __has_cpp_attribute(likely)                 // C++20 branch-prediction hints
    #define LIKELY   [[likely]]
    #define UNLIKELY [[unlikely]]
#else
    #define LIKELY
    #define UNLIKELY
#endif

NODISCARD("check the error code") int connect(const char* host, int port);

// Usage: the macro degrades to nothing on C++14, [[nodiscard]] on C++17,
// and [[nodiscard("check the error code")]] on C++20.

Replacing Preprocessor Macros with Modern C++

Every macro that can be replaced should be. Macros bypass name lookup, overload resolution, and the type system — they are invisible to debuggers and analysers. Modern C++ offers typed, scoped, tooling-friendly alternatives for the most common macro patterns.

Constants: inline constexpr instead of #define

cpp
// C++17
// BAD — no type, no scope, no debugger visibility
#define MAX_CONNECTIONS 128
#define PI              3.14159265358979323846

// GOOD — typed, scoped, ODR-safe across translation units (inline since C++17)
inline constexpr int    kMaxConnections = 128;
inline constexpr double kPi             = 3.14159265358979323846;

Conditional logic: if constexpr instead of #ifdef

Code inside #ifdef is invisible to the compiler on platforms where the guard is false — type errors and renamed APIs accumulate silently. if constexpr keeps all branches type-checked while still eliminating dead code at compile time.

cpp
// C++17
inline constexpr bool kDebugBuild =
#ifdef NDEBUG
    false;
#else
    true;
#endif

template<typename T>
void process(T value) {
    if constexpr (kDebugBuild) {
        // Always compiled — type errors caught on every platform
        validate_invariants(value);
    }
    do_work(value);
}

Return-value enforcement: [[nodiscard]] instead of wrapper macros

cpp
// C++17
// BAD — a macro that wraps every function to remind callers to check results
#define MUST_USE  // nothing, or a compiler extension

// GOOD — standard attribute, enforced by the compiler with a real diagnostic
[[nodiscard]] std::error_code write(const void* data, std::size_t n);
[[nodiscard("discarding fd leaks a file handle")]] int open_file(const char* path);  // C++20

Type constraints: concepts instead of STATIC_ASSERT macros

cpp
// C++20
// BAD — homebrew macro mimicking concept checks
#define REQUIRE_INTEGRAL(T) \
    static_assert(std::is_integral_v<T>, #T " must be integral")

// GOOD — concepts propagate to overload resolution and give cleaner diagnostics
template<std::integral T>
constexpr T clamp(T value, T lo, T hi) noexcept {
    return value < lo ? lo : value > hi ? hi : value;
}
// Calling clamp(1.5, 0.0, 2.0) is a hard error at the call site with a
// readable message; the REQUIRE_INTEGRAL macro fires deep inside the template.

Macros that remain genuinely irreplaceable — stringification (#), token pasting (##), __FILE__/__LINE__ capture in logging, and X-macro table generation — should be confined to implementation files or clearly documented internal headers, and named with an ALL_CAPS_PROJECT_PREFIX_ to signal their special status.


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