The preprocessor transforms preprocessing tokens before the C or C++ parser sees declarations, types, namespaces, or overloads. It expands includes, evaluates conditional directives, and performs macro replacement. Treat its output as generated source code, because language-level scope and type checking cannot repair an unsafe expansion.
Include Search Is a Build-Tool Contract #
#include "project/header.hpp" and #include <library/header> identify header-name tokens, not filesystem paths prescribed by the language standard. The mapping from a header name to a source file, including quote-versus-angle search order, is implementation-defined. Toolchains commonly search the including file’s directory and quote-specific paths for quotes, while angle headers begin in configured system paths; consult the compiler’s documentation and the build command for the actual order.
For GCC and Clang, inspect the active include search list with:
printf '' | c++ -E -x c++ -v -Use quoted names for project headers and angle names for installed or standard-library headers as a project convention. Configure third-party dependencies in the appropriate build target rather than adding broad global -I paths. -isystem has a different search position and can change diagnostic treatment, so it is not interchangeable with -I.
Every project header needs a multiple-inclusion strategy. A conventional guard is portable across conforming implementations:
#ifndef PROJECT_WIDGET_HPP
#define PROJECT_WIDGET_HPP
class Widget;
#endif#pragma once is widely supported but is not part of the ISO C or C++ standards. Use it only when the supported toolchains make that dependency acceptable.
Macro Replacement Has Several Stages #
For a function-like macro invocation, macro arguments are macro-expanded before substitution except where a parameter is used with # or ##. The replacement list is then rescanned for further expansion. A macro currently being expanded is temporarily unavailable for recursive expansion, which prevents direct infinite recursion.
#define STRINGIZE_IMPL(x) #x
#define STRINGIZE(x) STRINGIZE_IMPL(x)
#define VERSION 42
const char* version = STRINGIZE(VERSION); // "42"
The first layer passes an expanded VERSION to the implementation macro; direct #VERSION would produce "VERSION". Stringization preserves token spelling subject to the preprocessor’s whitespace and escaping rules. Token pasting with ## must form one valid preprocessing token.
#define JOIN_IMPL(a, b) a ## b
#define JOIN(a, b) JOIN_IMPL(a, b)
int JOIN(worker_, VERSION); // worker_42
Use generated identifiers sparingly. Pasted names hide declarations from ordinary code search and can produce diagnostics far from the macro call.
Conditional Compilation and Feature Tests #
Conditional directives operate on integer constant expressions after macro replacement. Test configuration macros explicitly and keep platform selection near the boundary that needs it.
#if defined(_WIN32)
# include <windows.h>
#elif defined(__unix__)
# include <unistd.h>
#else
# error "Unsupported platform"
#endifFor language features, use the standardized feature-test macros such as __cpp_concepts when the selected language mode exposes them. For library features, include the relevant header and test the corresponding __cpp_lib_* macro from <version> when available. __has_include can select an optional header on implementations that support the operator, but finding a header does not prove that its API or ABI matches the required version.
Use #error for an impossible configuration. Prefer a build-system diagnostic for combinations that can be detected before compilation, because the build system owns the target platform, compiler, and dependency versions.
Safe Macro Boundaries #
Prefer constexpr variables, inline functions, templates, enums, and scoped types whenever the language can express the operation. They preserve type checking, scope, overload resolution, and debugger visibility.
A macro that remains necessary must parenthesize each expression parameter and its complete expression result:
#define SQUARE(x) ((x) * (x))Parentheses do not make repeated evaluation safe: SQUARE(index++) increments twice. A macro must document whether each argument is evaluated once, and call sites should not pass expressions with side effects unless that behavior is intended.
Use the single-statement pattern for a macro that expands to statements:
#define LOG_FAILURE(message) \
do { \
log(message); \
} while (0)The pattern lets an if statement treat the invocation as one statement. It does not supply C++ scope, types, exception safety, or synchronization. Keep macros at portability, conditional-compilation, or token-generation boundaries, where their textual nature is required and contained.
Comments
Read or comment on GitHub (sign-in required to comment)