Preprocessor
C++ preprocessor — macros, include guards, conditional compilation,
Preprocessorsince C++98The 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:
- Splice lines ending with
\(physical → logical lines) - Tokenise into preprocessing tokens
- Expand
#includedirectives recursively - Expand macros
- Evaluate
#if/#elif/#else/#endifbranches - 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.
// Traditional include guard — portable, mandated by the standard
#ifndef MYLIB_WIDGET_HPP
#define MYLIB_WIDGET_HPP
// ... header content ...
#endif // MYLIB_WIDGET_HPP// #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:
#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
#endifObject-like and Function-like Macros
Object-like macros
// 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
// 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.
#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
// __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
#endifPredefined Macros
// 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 numberX 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:
// 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).
// 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__:
// __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.
#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
| Avoid | Prefer |
|---|---|
#define CONST 42 | constexpr int kConst = 42; (C++11) |
#define MAX(a,b) ... | template<typename T> constexpr T max(T,T) (C++11) |
#ifdef DEBUG logic branches | if constexpr (kDebug) (C++17) — always type-checked |
#define ASSERT(x) roll-your-own | static_assert for compile-time (C++11); assert() for runtime |
NULL | nullptr (C++11) |
ARRAY_SIZE(a) macro | std::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:
#define SQUARE(x) ((x) * (x))
int n = 3;
int bad = SQUARE(n++); // (n++) * (n++): undefined behaviour
// Fix: constexpr or inline function — arguments evaluate exactly onceMissing parentheses around arguments or result:
#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:
#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 andSTATIC_CHECKmacros - Attributes —
[[nodiscard]],[[deprecated]], and others that replace common warning-suppression macros - Modules — C++20 alternative to
#includeand header guards