Ch3.11: Preprocessor
What the Preprocessor Is
Before the compiler sees your C++ code, it is first processed by the preprocessor. The preprocessor performs simple text transformations on your source code. It does not understand C++ types, scopes, or semantics. It only manipulates tokens.
Preprocessor directives begin with # and are handled
line-by-line before compilation.
- copying files (
#include) - defining macros (
#define) - conditional compilation (
#ifdef,#ifndef) - embedding external data (
#embed) - compiler-specific directives (
#pragma)
#include
The #include directive literally copies the entire contents of
another file into the current file.
#include <fast_io.h>
#include <fast_io.h> // included twice
Why this does not break compilation
The fast_io library uses #pragma once at the top of
every header. When the preprocessor sees the header the second time, it
ignores its contents entirely.
This prevents duplicate definitions and makes repeated inclusion safe.
What happens without #pragma once
If a header does not use #pragma once (or an
include guard), including it twice inserts the same text twice:
// myheader.h (no pragma once)
void f();
#include "myheader.h"
#include "myheader.h" // inserted twice → duplicate definition error
This breaks compilation because the function is defined twice.
Why #include Is Slow
Because #include performs a full copy‑paste of the
header text, including all of its own #include directives,
large projects can become slow to compile.
A single header may expand into thousands of lines of code, and this expansion happens in every translation unit that includes it.
#pragma once
#pragma once ensures that a header file is included only once
per translation unit.
// myheader.h
#pragma once
void f();
This prevents duplicate definitions and speeds up compilation. All major compilers support it.
#define
The #define directive creates a macro.
Macros are simple text substitutions performed before compilation.
Object-like macros
#define PI 3.141592653589793
#define BUFFER_SIZE 1024
When the preprocessor sees PI, it replaces it with the text
3.141592653589793.
Function-like macros
#define SQR(x) ((x) * (x))
Parentheses are essential to avoid precedence bugs.
Macro side effects
Macros do not evaluate arguments once. They substitute text. This can cause dangerous side effects:
::std::uint_least32_t i{};
println(SQR(++i));
The macro expands to:
((++i) * (++i))
This increments i twice, which is almost never what you want.
Warnings about macros
- no type checking
- no scope (macros are global)
- hard to debug
- side effects when arguments contain
++or-- - prefer
constexprandinlinein modern C++
Macros are still necessary for conditional compilation and include guards.
#embed
The #embed directive embeds the contents of a file directly into
your program as data at compile time.
static constexpr auto data =
#embed "resource.bin";
Unlike #include, which pastes C++ code, #embed is used
for binary or text resources.
Support for #embed depends on the compiler and standard version.
Preprocessor Directives Summary
#include // copy-paste another file
#pragma once // include only once
#define // define a macro
#embed // embed external data
#ifdef/#ifndef // conditional compilation (next chapter)
#endif
Key takeaways
- The preprocessor runs before compilation: it performs text substitution.
- #include copies files: can be slow due to expansion.
- #pragma once: prevents multiple inclusion and avoids duplicate definitions.
- #define: simple but dangerous; watch out for side effects.
- Macros substitute text:
SQR(++i)expands to((++i)*(++i)). - #embed: embed binary/text data at compile time.
- Preprocessor has no understanding of C++: it only manipulates tokens.