C++ Module 4 – Functions, files and scope
C++ preprocessor and feature-test macros
How the C++ preprocessor includes files, expands macros and selects code, and how feature-test macros let one file build on compilers with different features.
What you will learn
- Predict what the preprocessor makes of a line, and check it with -E
- Choose a constexpr function, a template or a constant over a macro that repeats its argument
- Use #if, #elifdef and the operators # and ## to select and build code
- Check for a library feature with its __cpp_lib macro after including <version>
- Load a binary file into a program as array data with C++26 #embed
Before you start
What you’ll learn · Before you start
What you will learn
- Predict what the preprocessor makes of a line, and check it with -E
- Choose a constexpr function, a template or a constant over a macro that repeats its argument
- Use #if, #elifdef and the operators # and ## to select and build code
- Check for a library feature with its __cpp_lib macro after including <version>
- Load a binary file into a program as array data with C++26 #embed
Before you start
On this page
#define SQ_BAD(x) x * x looks like a function, yet with a equal to 4, SQ_BAD(a + 1) is 9, not 25: the
preprocessor: The first stage of a C++ build. It joins continued lines, replaces comments with spaces, runs the lines that start with #, such as #include and #define, and expands macros, so the compiler gets the rewritten text. -E shows that text. pasted the text a + 1 * a + 1 before the compiler applied a single rule
of precedence. The same stage decides which code is compiled at all: of the 15 feature-test macro: A macro such as __cpp_modules or __cpp_lib_modules whose value, a year and month such as 202207, says that a compiler or its library claims a feature. A toolchain may build a feature without defining its macro, or define the macro without the feature. More in “C++20 modules: export, import and partitions” this lesson prints,
7 differ between Apple clang 21 with libc++ and GCC 16 with libstdc++ in C++23 mode, and 10 in C++26 mode.
You will predict what the preprocessor makes of a file and check it with -E, write macro: A name defined with #define that the preprocessor replaces by its text wherever it appears later in the file, as SQ(a + 1) becomes ((a + 1) * (a + 1)). A macro has no type and no scope, and its arguments are pasted as text. that survive their
arguments and know when not to write one, keep one source file working on both libraries with checks that cannot
silently fail, and turn a file’s bytes into data with C++26’s #embed. Most programs were built with both Apple clang 21 and GCC 16.
The preprocessor rewrites the text before the compiler sees it
The preprocessor is the first stage of every C++ build. It reads the lines that begin with #, called directives,
and rewrites the text of the file before the compiler sees it: #include pastes in other files, #define creates
macros that are replaced by their text wherever they appear, and #if keeps or drops lines. It
works on text and tokens only, so it knows nothing about types, scopes or namespaces.
Each directive is one line that starts with #. They fall into five groups:
- Files:
#include <file>and#include "file"paste in the text of a header;#embed(C++26) pastes in the bytes of any file as a list of numbers. - Macros:
#definecreates a name that is replaced by text, and#undefremoves it. - Conditions:
#if,#ifdef,#ifndef,#elif,#elseand#endif, plus#elifdefand#elifndefsince C++23, keep the lines of the first true branch and drop the rest. - Messages:
#errorstops the build with a message;#warning, standard since C++23, prints one and goes on. - Other:
#pragmapasses an instruction specific to the compiler, and#linechanges the line number and file name that messages report.
Inside #if and #elif, four operators ask questions: defined(NAME) (is a macro defined?), __has_include(<header>)
(C++17: can this header be found?), __has_cpp_attribute(name) (C++20: is this attribute supported?) and
__has_embed("file") (C++26: can this file be embedded?). They work only inside those directives, not in ordinary
code. Step through what happens to a short file of the next section, square_macro.cpp, before the compiler gets
it:
The drawing shows the whole scene; its 7 steps are listed below.
Every step as text
- Lines. The preprocessor reads square_macro.cpp line by line. A line that ends in a backslash is joined to the next one, with no space added, so a long #define can be split over several lines.
- Comments. Every comment is replaced by a space, so none of them reaches the compiler, and the -E output of the session shows none.
- Tokens. The text is split into preprocessing tokens: names such as SQ_BAD and a, numbers such as 1, and punctuators such as * and (. Macros work on these tokens, never on the letters inside a name.
- #include, first. #include <iostream> is the first directive. The preprocessor reads the whole header, and every header it includes, through these same stages and pastes the result in place of the line.
- #define. #define SQ_BAD(x) x * x records a macro and leaves an empty line behind. Directives run in the order they appear, so a macro exists from its #define to the end of the file, or to an #undef.
- Expansion. In main, SQ_BAD(a + 1) is replaced by the macro's text with the argument pasted in for x, a + 1 * a + 1. No rule of precedence has been applied yet.
- Compilation. -E stops here, which is how the session printed the five std::cout lines. The compiler then joins adjacent string literals into one and compiles the translation unit, applying precedence for the first time: 4 + 4 + 1 is 9.
The text reaches the compiler only after every directive has run and every macro has been replaced. To see that text,
stop the build after the preprocessor with -E. Add -P to drop the line markers that tell the compiler which file
each line came from.
Checkpoint
Check: What the preprocessor does first
A macro pastes text, so its arguments ignore precedence
A function-like macro looks like a function call, but its argument is pasted into the replacement text before any C++ rule applies. That is the root of the classic macro bug:
// square_macro.cpp: a function-like macro without parentheses, the fixed macro, and a function.
#include <iostream>
#define SQ_BAD(x) x * x
#define SQ(x) ((x) * (x))
constexpr int sq(int x) { return x * x; }
int main() {
int a = 4;
std::cout << "SQ_BAD(a + 1) = " << SQ_BAD(a + 1) << '\n';
std::cout << "SQ(a + 1) = " << SQ(a + 1) << '\n';
std::cout << "sq(a + 1) = " << sq(a + 1) << '\n';
std::cout << "100 / SQ_BAD(5) = " << 100 / SQ_BAD(5) << '\n';
std::cout << "100 / SQ(5) = " << 100 / SQ(5) << '\n';
} from session import run
# Run it, then look at what the compiler was really given: -E stops after preprocessing, -P drops line markers.
run("""
clang++ -std=c++23 -Wall -Wextra square_macro.cpp -o square && ./square
g++-16 -std=c++23 -Wall -Wextra square_macro.cpp -o square && ./square
clang++ -std=c++23 -E -P square_macro.cpp | grep 'std::cout <<'
""") Output
$ clang++ -std=c++23 -Wall -Wextra square_macro.cpp -o square && ./square
SQ_BAD(a + 1) = 9
SQ(a + 1) = 25
sq(a + 1) = 25
100 / SQ_BAD(5) = 100
100 / SQ(5) = 4
$ g++-16 -std=c++23 -Wall -Wextra square_macro.cpp -o square && ./square
SQ_BAD(a + 1) = 9
SQ(a + 1) = 25
sq(a + 1) = 25
100 / SQ_BAD(5) = 100
100 / SQ(5) = 4
$ clang++ -std=c++23 -E -P square_macro.cpp | grep 'std::cout <<'
std::cout << "SQ_BAD(a + 1) = " << a + 1 * a + 1 << '\n';
std::cout << "SQ(a + 1) = " << ((a + 1) * (a + 1)) << '\n';
std::cout << "sq(a + 1) = " << sq(a + 1) << '\n';
std::cout << "100 / SQ_BAD(5) = " << 100 / 5 * 5 << '\n';
std::cout << "100 / SQ(5) = " << 100 / ((5) * (5)) << '\n';
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 square_session.py
The last command shows the five std::cout lines exactly as the compiler received them. Step through the two macros
and watch where precedence finally applies:
The drawing shows the whole scene; its 5 steps are listed below.
Every step as text
- The argument's text. The preprocessor replaces x in x * x by the text of the argument, a + 1, exactly as written. It knows nothing about the operators in it.
- Precedence, too late. Only now does the compiler apply precedence. * binds tighter than +, so the text means a + (1 * a) + 1, which is 4 + 4 + 1 = 9, not 25.
- Parentheses everywhere. SQ puts parentheses around each use of x and around the whole body, ((x) * (x)). The same argument now gives ((a + 1) * (a + 1)).
- One group per argument. Each pasted argument is now a group of its own: 5 * 5 = 25, the square the macro was meant to give.
- The outer pair. 100 / SQ_BAD(5) becomes 100 / 5 * 5, which groups as (100 / 5) * 5 and gives 100. 100 / SQ(5) becomes 100 / ((5) * (5)), which gives 4: the outer parentheses keep the body together.
SQ_BAD(a + 1) becomes a + 1 * a + 1, which multiplication binds as a + (1 * a) + 1: 9, not 25. 100 / SQ_BAD(5)
becomes 100 / 5 * 5, which is 100. The fixed macro puts parentheses around every use of the parameter and around
the whole replacement: ((x) * (x)).
Parenthesising the parameters but not the body
#define SQ(x) (x) * (x) fixes SQ(a + 1), but 100 / SQ(5) still becomes 100 / (5) * (5), which is 100. Put
parentheses around each use of a parameter and around the whole replacement: ((x) * (x)).
Checkpoint
Check: Macros paste text
A macro repeats its argument and ignores namespaces
The fixed SQ still has a flaw that no parentheses can fix, because the argument is pasted in once for each use:
// twice.cpp: a macro argument is pasted in once per use, so it is evaluated once per use.
#include <iostream>
#define SQ(x) ((x) * (x))
constexpr int sq(int x) { return x * x; }
int next_id() {
static int id = 0; // counts the calls
return ++id;
}
int main() {
std::cout << "SQ(next_id()) = " << SQ(next_id()) << '\n'; // calls next_id() twice: 1 * 2
std::cout << "sq(next_id()) = " << sq(next_id()) << '\n'; // calls it once: 3 * 3
std::cout << "next_id() was called " << next_id() - 1 << " times\n";
} // unsequenced.cpp: with i++ as the argument, the expansion modifies i twice with no order between the two.
#define SQ(x) ((x) * (x))
int square_of_next(int i) {
return SQ(i++);
} from session import run
# twice.cpp is well defined, just surprising. unsequenced.cpp is undefined behaviour: both compilers warn.
run("""
clang++ -std=c++23 -Wall -Wextra twice.cpp -o twice && ./twice
clang++ -std=c++23 -Wall -Wextra -fsyntax-only unsequenced.cpp
g++-16 -std=c++23 -Wall -Wextra -fsyntax-only unsequenced.cpp
""") Output
$ clang++ -std=c++23 -Wall -Wextra twice.cpp -o twice && ./twice
SQ(next_id()) = 2
sq(next_id()) = 9
next_id() was called 3 times
$ clang++ -std=c++23 -Wall -Wextra -fsyntax-only unsequenced.cpp
unsequenced.cpp:5:16: warning: multiple unsequenced modifications to 'i' [-Wunsequenced]
5 | return SQ(i++);
| ^~
unsequenced.cpp:2:17: note: expanded from macro 'SQ'
2 | #define SQ(x) ((x) * (x))
| ^ ~
1 warning generated.
$ g++-16 -std=c++23 -Wall -Wextra -fsyntax-only unsequenced.cpp
unsequenced.cpp: In function 'int square_of_next(int)':
unsequenced.cpp:5:16: warning: operation on 'i' may be undefined [-Wsequence-point]
5 | return SQ(i++);
| ^
unsequenced.cpp:2:17: note: in definition of macro 'SQ'
2 | #define SQ(x) ((x) * (x))
| ^
unsequenced.cpp:5:16: warning: operation on 'i' may be undefined [-Wsequence-point]
unsequenced.cpp:2:17: note: in definition of macro 'SQ'
2 | #define SQ(x) ((x) * (x))
| ^
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 twice_session.py
SQ(next_id()) called the function twice and multiplied two different results, 1 and 2. With i++ as the argument,
the expansion changes i twice in one expression with no order between the changes, which is
undefined behaviour: What a program does when it breaks a rule for which the C++ standard sets no requirements, such as reading past the end of an array. The compiler may assume it never happens, so the result can change with the compiler and the optimisation level. More in “Object lifetime and undefined behaviour in C++”; both compilers warn, in their own words. The constexpr
function sq() has none of these problems: it evaluates its argument once, checks its type, and still runs at compile
time when its argument is a constant.
A macro also ignores namespaces. It replaces its name everywhere after its #define, so a macro called max from
some old header breaks every later call of std::max:
// max_macro.cpp: some old header in the build defines a macro called max.
#include <algorithm>
#include <iostream>
#define max(a, b) ((a) > (b) ? (a) : (b))
int main() {
std::cout << std::max(3, 7) << '\n'; // the preprocessor rewrites std::max(3, 7) first
} from session import run
# A macro has no namespace: it replaces the word max wherever a ( follows it.
run("""
clang++ -std=c++23 -c max_macro.cpp -o max.o
g++-16 -std=c++23 -c max_macro.cpp -o max.o
""") Output
$ clang++ -std=c++23 -c max_macro.cpp -o max.o
max_macro.cpp:8:23: error: expected unqualified-id
8 | std::cout << std::max(3, 7) << '\n'; // the preprocessor rewrites std::max(3, 7) first
| ^
max_macro.cpp:5:19: note: expanded from macro 'max'
5 | #define max(a, b) ((a) > (b) ? (a) : (b))
| ^
1 error generated.
[exit status 1]
$ g++-16 -std=c++23 -c max_macro.cpp -o max.o
max_macro.cpp: In function 'int main()':
max_macro.cpp:5:19: error: expected unqualified-id before '(' token
5 | #define max(a, b) ((a) > (b) ? (a) : (b))
| ^
max_macro.cpp:8:23: note: in expansion of macro 'max'
8 | std::cout << std::max(3, 7) << '\n'; // the preprocessor rewrites std::max(3, 7) first
| ^~~
[exit status 1]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 max_session.py
The compiler sees std::((3) > (7) ? (3) : (7)), which is not C++. Writing (std::max)(3, 7) works around it,
because a function-like macro expands only when its name is followed by (, but the real fix is to remove the macro
or #undef it after the header that defines it.
So for constants use constexpr variables, for small computations constexpr or inline functions, and for code
that works on many types templates: they have types and scopes, and debuggers and error messages know their names.
Macros remain the right tool for what only the preprocessor can do: include guards, conditional compilation: Keeping or dropping lines before compilation with #if, #ifdef, #elif and their relatives: only the first branch whose condition is true reaches the compiler, so the others may even use features one compiler lacks., and
capturing the call site with __FILE__ and __LINE__ (since C++20, std::source_location does that last job in
ordinary code). Say what the code you are about to write has to do, and the table marks what fits:
Answer the question above: the table then marks what fits your case and says why the rest does not.
| Write it as | Arguments once | Types and scopes | Usable in #if | Knows the call site |
|---|---|---|---|---|
| A macro (#define) What the compiler sees: text pasted wherever the name appears | Poor SQ(next_id()) called next_id() twice and gave 1 * 2 | Poor a macro named max broke std::max(3, 7) | Good the only names #if can test | Good __FILE__ and __LINE__ expand where it is used |
| A constexpr function What the compiler sees: a real function, run at compile time when it can be | Good sq(next_id()) called it once and gave 3 * 3 | Good a function with a type, in a namespace | Poor the preprocessor knows no functions | Mixed only with std::source_location, the last row |
| A function template What the compiler sees: one function for each argument type | Good a function: each argument evaluated once | Good types checked for each use | Poor the preprocessor knows no templates | Mixed only with std::source_location, the last row |
| A constexpr variable What the compiler sees: a typed constant in a namespace | Good no arguments: one value | Good has a type, a name and a namespace | Poor #if sees macros, not variables | Poor a value has no caller |
| std::source_location (C++20) What the compiler sees: the caller's file and line, in ordinary code | Good an ordinary function call | Good ordinary C++ | Poor #if sees macros, not functions | Good reports the caller's file and line (C++20) |
How the answers decide
- “What must the code do?” Compute a value from its arguments: rules out A macro (#define). A macro pastes its argument once per use: SQ(next_id()) ran next_id() twice, and SQ(i++) is undefined behaviour.
- “What must the code do?” Compute a value from its arguments: rules out A constexpr variable and std::source_location (C++20). A constant takes no arguments, and std::source_location reports where a call was made rather than computing anything.
- “What must the code do?” Name a constant: adds a caveat to A macro (#define). A #define works, but it has no type and no namespace: a macro named max broke every later std::max.
- “What must the code do?” Name a constant: adds a caveat to A constexpr function. A function that returns a constant works, but a constexpr variable says the same thing more plainly.
- “What must the code do?” Name a constant: rules out A function template and std::source_location (C++20). Neither names a value. A constexpr variable does, with a type and a scope.
- “What must the code do?” Report the file and line it is used on: rules out A constexpr variable. A value has no caller, so it cannot say where it was used.
- “What must the code do?” Report the file and line it is used on: rules out A constexpr function and A function template. On their own, a function and a template know nothing about their caller; std::source_location, the last row, does that job.
- “What must the code do?” Report the file and line it is used on: adds a caveat to A macro (#define). __FILE__ and __LINE__ in a macro work, and since C++20 std::source_location does the same job in ordinary code.
- “What must the code do?” Keep or drop lines before compiling: rules out A constexpr function, A function template, A constexpr variable and std::source_location (C++20). Only the preprocessor runs before the compiler: include guards and feature checks need #if and macros.
Key idea
A macro pastes its argument’s text once per use, without types or scopes: write a constexpr function, a template
or a constexpr variable unless the job needs the preprocessor itself.
Checkpoint
Check: Repeated arguments and namespaces
Conditional compilation keeps one branch; macro operators build tokens
Three operators exist only inside macros. #x turns an argument into a string literal (“stringizing”), a##b
glues two tokens into one, and __VA_OPT__(,) (C++20) inserts a comma only when a variadic macro received extra
arguments. The conditions in this file also use two directives that became standard in C++23, #elifdef and
#warning:
// directives.cpp: stringizing (#), token pasting (##), __VA_OPT__, #elifdef and #warning.
#include <cstdio>
#define TO_TEXT(x) #x
#define GLUE(a, b) a##b
#define LOG(fmt, ...) std::printf("[log] " fmt "\n" __VA_OPT__(,) __VA_ARGS__)
#if defined(__clang__)
# define COMPILER "Clang"
#elifdef __GNUC__
# define COMPILER "GCC"
#else
# define COMPILER "another compiler"
#endif
#ifndef NDEBUG
# warning "this is a debug build: NDEBUG is not defined"
#endif
int main() {
int GLUE(order, _count) = 3; // the preprocessor turns this into: int order_count = 3;
std::printf("%s = %d\n", TO_TEXT(order_count * 2), order_count * 2);
LOG("built by " COMPILER);
LOG("%d orders", order_count);
std::printf("line %d of %s\n", __LINE__, __FILE__);
} from session import run
# C++23 mode on both compilers, then the same file as C++20 with -pedantic, then a build with -DNDEBUG.
run("""
clang++ -std=c++23 -Wall -Wextra directives.cpp -o directives && ./directives
g++-16 -std=c++23 -Wall -Wextra directives.cpp -o directives && ./directives
clang++ -std=c++20 -pedantic -fsyntax-only directives.cpp 2>&1 | grep -c 'C++23 extension'
clang++ -std=c++23 -DNDEBUG -Wall -Wextra directives.cpp -o directives && ./directives | head -n 1
""") Output
$ clang++ -std=c++23 -Wall -Wextra directives.cpp -o directives && ./directives
directives.cpp:17:4: warning: "this is a debug build: NDEBUG is not defined" [-W#warnings]
17 | # warning "this is a debug build: NDEBUG is not defined"
| ^
1 warning generated.
order_count * 2 = 6
[log] built by Clang
[log] 3 orders
line 25 of directives.cpp
$ g++-16 -std=c++23 -Wall -Wextra directives.cpp -o directives && ./directives
directives.cpp:17:4: warning: #warning "this is a debug build: NDEBUG is not defined" [-Wcpp]
17 | # warning "this is a debug build: NDEBUG is not defined"
| ^~~~~~~
order_count * 2 = 6
[log] built by GCC
[log] 3 orders
line 25 of directives.cpp
$ clang++ -std=c++20 -pedantic -fsyntax-only directives.cpp 2>&1 | grep -c 'C++23 extension'
2
$ clang++ -std=c++23 -DNDEBUG -Wall -Wextra directives.cpp -o directives && ./directives | head -n 1
order_count * 2 = 6
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 directives_session.py
Step through the file as Clang’s preprocessor reads it: the directives first, then the macros that main uses.
The drawing shows the whole scene; its 8 steps are listed below.
Every step as text
- The header. #include <cstdio> pastes the declarations of the C library's printf, which main uses for every line it prints.
- Three macros. The three #define lines record TO_TEXT, GLUE and LOG. Each line is removed from the text; the macros exist from here to the end of the file.
- One branch kept. Clang defines __clang__, so the first branch is kept and COMPILER becomes "Clang"; the #elifdef and #else branches are dropped. The test comes first because Clang also defines __GNUC__, which GCC's branch checks.
- A warning on purpose. NDEBUG is not defined, so the #warning line is kept, and both compilers print this is a debug build. With -DNDEBUG on the command line, the whole branch is dropped and nothing is printed.
- Token pasting. GLUE(order, _count) glues its two arguments into one token, so the line becomes int order_count = 3;, a variable whose name appears nowhere in the source.
- Stringizing. #x turns the argument into a string literal: TO_TEXT(order_count * 2) becomes "order_count * 2", and printf prints it before the value, 6.
- An optional comma. LOG("built by " COMPILER) passes no extra argument, so __VA_OPT__(,) leaves out the comma that would break the call. The program prints [log] built by Clang; GCC's build prints built by GCC.
- Where am I. __LINE__ and __FILE__ expand where they are written, so the last line prints line 25 of directives.cpp.
TO_TEXT(order_count * 2)became the string"order_count * 2", andGLUE(order, _count)became the variable nameorder_count.LOG("built by " COMPILER)has no extra argument, so__VA_OPT__(,)left out the comma that would otherwise break the call toprintf.#if defined(__clang__)is tested first because Clang also defines__GNUC__, for compatibility with code written for GCC. Each compiler took its own branch.- In C++20 mode with
-pedantic, Clang counts two C++23 extensions in the file:#elifdefand#warning. -DNDEBUGon the command line definesNDEBUGbefore the first line of the file, so the#warningbranch is dropped. Many release builds defineNDEBUG, which also turns everyassert()into nothing.
Choosing lines this way is called conditional compilation: the branches that are dropped are never compiled, so they may even use features that one of the compilers lacks.
Checkpoint
Check: Conditions and macro operators
Feature-test macros say what a compiler and its library support
Feature-test macros are macros the compiler and its standard library define to
say what they support. There are two families. Language feature macros, such as __cpp_explicit_this_parameter,
are predefined macro: A macro the compiler defines before it reads your file, such as __cplusplus, __clang__ or __GNUC__, so that code can test which compiler and which edition of C++ are building it. More in “What C++ is used for today and how to learn it” by the compiler. Library feature macros, such as
__cpp_lib_print, are defined by the standard library: by the header of the feature and, since C++20, all together
by <version>, a header that declares nothing else. After #include <version>, __cpp_lib_print is defined only if
the library has std::print.
The standing document SD-6 of the C++ committee gives each macro a value based on the year and month in which its
feature was voted into the working draft, and a later revision of the same feature raises the value, so a test such
as >= 202302L asks for that revision of the feature or a later one. This program prints a selection of macros, and
the session builds it with both compilers in C++23 and C++26 mode:
// features.cpp: prints feature-test macros of the compiler and its standard library.
// A defined macro expands to its value, such as 202207L; an undefined name stays as it is,
// so the text of an undefined one starts with an underscore and is printed as "-".
#include <version>
#include <cstdio>
#define AS_TEXT(x) #x
#define VALUE(x) AS_TEXT(x)
#define SHOW(macro) show(#macro, VALUE(macro))
static void show(const char* name, const char* value) {
std::printf("%-30s %s\n", name, value[0] == '_' ? "-" : value);
}
int main() {
SHOW(__cplusplus);
SHOW(__cpp_lib_print);
SHOW(__cpp_lib_format);
SHOW(__cpp_lib_expected);
SHOW(__cpp_lib_mdspan);
SHOW(__cpp_lib_flat_map);
SHOW(__cpp_lib_generator);
SHOW(__cpp_lib_ranges_enumerate);
SHOW(__cpp_lib_ranges_chunk);
SHOW(__cpp_lib_move_only_function);
SHOW(__cpp_lib_function_ref);
SHOW(__cpp_lib_inplace_vector);
SHOW(__cpp_lib_to_chars);
SHOW(__cpp_lib_stacktrace);
SHOW(__cpp_explicit_this_parameter);
SHOW(__cpp_pp_embed);
} from session import run
# One build per compiler and edition; paste puts the four columns side by side.
run("""
clang++ -std=c++23 features.cpp -o f1 && ./f1 > clang-23.txt
g++-16 -std=c++23 features.cpp -o f2 && ./f2 > gcc-23.txt
clang++ -std=c++26 features.cpp -o f3 && ./f3 > clang-26.txt
g++-16 -std=c++26 features.cpp -o f4 && ./f4 > gcc-26.txt
echo "macro clang-C++23 gcc-C++23 clang-C++26 gcc-C++26" > table.txt
paste clang-23.txt gcc-23.txt clang-26.txt gcc-26.txt | awk '{print $1, $2, $4, $6, $8}' >> table.txt
column -t table.txt
""", max_lines=40) Output
$ clang++ -std=c++23 features.cpp -o f1 && ./f1 > clang-23.txt
$ g++-16 -std=c++23 features.cpp -o f2 && ./f2 > gcc-23.txt
$ clang++ -std=c++26 features.cpp -o f3 && ./f3 > clang-26.txt
$ g++-16 -std=c++26 features.cpp -o f4 && ./f4 > gcc-26.txt
$ echo "macro clang-C++23 gcc-C++23 clang-C++26 gcc-C++26" > table.txt
$ paste clang-23.txt gcc-23.txt clang-26.txt gcc-26.txt | awk '{print $1, $2, $4, $6, $8}' >> table.txt
$ column -t table.txt
macro clang-C++23 gcc-C++23 clang-C++26 gcc-C++26
__cplusplus 202302L 202302L 202400L 202400L
__cpp_lib_print 202207L 202406L 202207L 202406L
__cpp_lib_format 202110L 202304L 202110L 202603L
__cpp_lib_expected 202211L 202211L 202211L 202211L
__cpp_lib_mdspan 202207L 202207L 202406L 202406L
__cpp_lib_flat_map 202511L 202511L 202511L 202511L
__cpp_lib_generator - 202207L - 202207L
__cpp_lib_ranges_enumerate - 202302L - 202302L
__cpp_lib_ranges_chunk - 202202L - 202202L
__cpp_lib_move_only_function - 202110L - 202110L
__cpp_lib_function_ref - - - 202603L
__cpp_lib_inplace_vector - - - 202603L
__cpp_lib_to_chars - 201611L - 202306L
__cpp_lib_stacktrace - - - -
__cpp_explicit_this_parameter 202110L 202110L 202110L 202110L
__cpp_pp_embed - - - 202502L
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 features_session.py
The table is the reason this track writes guards. In both modes, libc++ has no std::generator, no
views::enumerate, no views::chunk and no std::move_only_function, all of which GCC 16’s library provides; in
C++26 mode GCC 16 adds std::function_ref, std::inplace_vector and #embed. The library macros also differ in
value where both have a feature: GCC’s __cpp_lib_print is a later revision than libc++’s.
__cplusplus, the first row, names the edition itself in the same year-and-month form. Walk through its values:
- C++98
199711L
The value for the 1998 standard, the first ISO edition of C++. A test such as __cplusplus >= 201103L asks for anything newer.
- C++11
201103L
The value for the 2011 standard. #if __cplusplus >= 201103L is the classic test for it.
- C++14
201402L
The value for the 2014 standard.
- C++17
201703L
The value for the 2017 standard, which added __has_include, the test for a header that this lesson uses.
- C++20
202002L
The value for the 2020 standard, which added the <version> header: it defines every library feature-test macro and declares nothing else.
- C++23
202302L the mode of this track
The value for the 2023 standard, which made #elifdef, #elifndef and #warning standard. Both compilers printed 202302L with -std=c++23.
- C++26
202603L measured 202400L
GCC's manual gives 202603L for the 2026 standard. In -std=c++26 mode, Apple clang 21 and GCC 16 both printed 202400L, a value of their own that is larger than the C++23 one.
The timeline has 7 events, from C++98 to C++26.
__cplusplus was 202400L in C++26 mode on both compilers: their working value for the new edition, which GCC’s
manual gives as 202603L for the 2026 standard. So test the macro of the feature you need rather than the edition:
a compiler can support an edition in part, and the feature macro says which part.
Checkpoint
Check: Feature-test macros and their values
Test a library feature after <version>, not by its header
A library macro exists only after a standard header has defined it. A check placed above every #include therefore
sees nothing:
C++ (too_early.cpp) · too_early.cpp
// too_early.cpp: the check runs before any standard header is included,
// so __cpp_lib_print is not defined yet, whatever the library supports.
#if defined(__cpp_lib_print)
# define HAVE_PRINT 1
#else
# define HAVE_PRINT 0
#endif
#include <iostream>
int main() {
std::cout << "std::print available? " << (HAVE_PRINT ? "yes" : "no") << '\n';
} C++ (checked.cpp) · checked.cpp
// checked.cpp: <version> first. It defines every library feature-test macro, and nothing else.
#include <version>
#if defined(__cpp_lib_print)
# define HAVE_PRINT 1
#else
# define HAVE_PRINT 0
#endif
#include <iostream>
int main() {
std::cout << "std::print available? " << (HAVE_PRINT ? "yes" : "no") << '\n';
} from session import run
run("""
clang++ -std=c++23 too_early.cpp -o too_early && ./too_early
clang++ -std=c++23 checked.cpp -o checked && ./checked
g++-16 -std=c++23 too_early.cpp -o too_early && ./too_early
g++-16 -std=c++23 checked.cpp -o checked && ./checked
""") Output
$ clang++ -std=c++23 too_early.cpp -o too_early && ./too_early std::print available? no $ clang++ -std=c++23 checked.cpp -o checked && ./checked std::print available? yes $ g++-16 -std=c++23 too_early.cpp -o too_early && ./too_early std::print available? no $ g++-16 -std=c++23 checked.cpp -o checked && ./checked std::print available? yes
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 version_session.py
Both libraries have std::print, but the early check reported “no” on both, without any warning. Put
#include <version> first in every file that tests a library macro.
__has_include(<header>) answers a narrower question: can the file be found? A library can ship a header while the
feature in it is switched off for that build, and only the feature macro says so:
// header_or_feature.cpp: is the header there, and is the feature switched on?
// __has_include works only inside #if and #elif, so each answer becomes a constant first.
#include <version>
#include <cstdio>
#if __has_include(<print>)
constexpr bool print_header = true;
#else
constexpr bool print_header = false;
#endif
#if __has_include(<generator>)
constexpr bool generator_header = true;
#else
constexpr bool generator_header = false;
#endif
#if __has_include(<stacktrace>)
constexpr bool stacktrace_header = true;
#else
constexpr bool stacktrace_header = false;
#endif
#define AS_TEXT(x) #x
#define VALUE(x) AS_TEXT(x)
static void report(const char* header, bool found, const char* macro) {
std::printf("%-13s header %-8s feature macro %s\n", header, found ? "found," : "missing,", macro[0] == '_' ? "-" : macro);
}
int main() {
report("<print>", print_header, VALUE(__cpp_lib_print));
report("<generator>", generator_header, VALUE(__cpp_lib_generator));
report("<stacktrace>", stacktrace_header, VALUE(__cpp_lib_stacktrace));
} // trust_header.cpp: uses std::stacktrace whenever the header exists.
#if __has_include(<stacktrace>)
# include <stacktrace>
# include <iostream>
int main() {
std::cout << std::stacktrace::current() << '\n';
}
#else
int main() {}
#endif from session import run
# GCC 16 on this Mac ships <stacktrace>, but its library was built without the feature.
run("""
clang++ -std=c++23 header_or_feature.cpp -o hf && ./hf
g++-16 -std=c++23 header_or_feature.cpp -o hf && ./hf
g++-16 -std=c++23 -c trust_header.cpp -o trust.o
""") Output
$ clang++ -std=c++23 header_or_feature.cpp -o hf && ./hf
<print> header found, feature macro 202207L
<generator> header missing, feature macro -
<stacktrace> header missing, feature macro -
$ g++-16 -std=c++23 header_or_feature.cpp -o hf && ./hf
<print> header found, feature macro 202406L
<generator> header found, feature macro 202207L
<stacktrace> header found, feature macro -
$ g++-16 -std=c++23 -c trust_header.cpp -o trust.o
trust_header.cpp: In function 'int main()':
trust_header.cpp:7:23: error: 'std::stacktrace' has not been declared
7 | std::cout << std::stacktrace::current() << '\n';
| ^~~~~~~~~~
trust_header.cpp:5:1: note: 'std::stacktrace' is defined in header '<stacktrace>'; this is probably fixable by adding '#include <stacktrace>'
4 | # include <iostream>
+++ |+#include <stacktrace>
5 |
[exit status 1]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 header_session.py
GCC 16 on this Mac has a <stacktrace> header, but its library was built without stack-trace support, so the header
declares nothing and __cpp_lib_stacktrace stays undefined. A file that trusted __has_include fails to compile,
with a suggestion to add the include it already has. The rule that follows:
Deciding whether a library feature can be used
Text description of the diagram
The flowchart shows how one source file decides at compile time whether to use a feature of the standard library, called X here.
- Include <version> at the top of the file. It defines the feature-test macros of the standard library.
- Ask whether __cpp_lib_X is defined. If it is not, use the fallback code.
- If it is defined, ask whether its value is at least the revision your code needs, such as 202302L. If it is not, use the fallback code.
- If both answers are yes, use the feature.
A note beside the flowchart explains why the check uses the macro rather than __has_include: a header can exist while the feature in it is switched off, as the stacktrace header of GCC 16 on macOS shows.
Use __has_include for headers outside the standard library, such as an optional third-party library, and the
feature-test macro, after <version>, for everything the standard defines.
Testing a library macro before any #include
#if defined(__cpp_lib_print) on the first line of a file is always false, and it takes the fallback without a
warning on every library. Include <version> above the test.
Checkpoint
Check: Checking a feature safely
#embed turns a file’s bytes into array data
C++26 adds #embed, which pastes the contents of a file into the program as a comma-separated list of numbers, one per
byte. It replaces the old routine of converting icons, certificates or test data into a huge generated array by an
extra build step:
// embed_demo.cpp: #embed pastes the bytes of a file into the program as a list of numbers.
#include <cstdio>
constexpr unsigned char greeting[] = {
#embed "greeting.txt"
};
int main() {
std::printf("%zu bytes:", sizeof greeting);
for (unsigned char c : greeting) std::printf(" %d", c);
std::printf("\nas text: %.*s\n", static_cast<int>(sizeof greeting), reinterpret_cast<const char*>(greeting));
} from session import run
# greeting.txt holds the 7 bytes of "Namaste". -pedantic asks each compiler to flag anything non-standard.
run("""
wc -c greeting.txt
g++-16 -std=c++26 -Wall -Wextra -pedantic embed_demo.cpp -o embed && ./embed
g++-16 -std=c++23 -Wall -Wextra -pedantic embed_demo.cpp -o embed
clang++ -std=c++26 -Wall -Wextra -pedantic embed_demo.cpp -o embed && ./embed
""") Output
$ wc -c greeting.txt
7 greeting.txt
$ g++-16 -std=c++26 -Wall -Wextra -pedantic embed_demo.cpp -o embed && ./embed
7 bytes: 78 97 109 97 115 116 101
as text: Namaste
$ g++-16 -std=c++23 -Wall -Wextra -pedantic embed_demo.cpp -o embed
embed_demo.cpp:5:2: warning: '#embed' before C++26 is a GCC extension [-Wc++26-extensions]
5 | #embed "greeting.txt"
| ^~~~~
$ clang++ -std=c++26 -Wall -Wextra -pedantic embed_demo.cpp -o embed && ./embed
embed_demo.cpp:5:2: warning: #embed is a Clang extension [-Wc23-extensions]
5 | #embed "greeting.txt"
| ^
1 warning generated.
7 bytes: 78 97 109 97 115 116 101
as text: Namaste
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 embed_session.py
Step from the file on disk to the numbers the program prints:
The drawing shows the whole scene; its 5 steps are listed below.
Every step as text
- The file. greeting.txt holds the seven characters of Namaste and nothing else: no newline at the end, so wc -c counts 7 bytes.
- The directive. #embed "greeting.txt" sits between the braces of the array's initialiser. The preprocessor reads the file while the program is being built.
- Numbers, one per byte. The directive is replaced by the bytes as a comma-separated list: 78, 97, 109, 97, 115, 116, 101. The array greeting gets 7 elements, and the program reads no file when it runs.
- The output. The program prints the 7 numbers, then the same bytes as text: 78 is N, 97 is a, 109 is m, and the seven spell Namaste.
- Standard or extension. GCC 16 accepts #embed as standard C++26, and with -pedantic flags it only in C++23 mode. Clang 21 builds it too, but -pedantic calls it a Clang extension even in C++26 mode.
GCC has implemented the C++26 directive since GCC 15: in C++26 mode it is standard, and with -pedantic GCC flags it
only in earlier modes. Clang 21 accepts #embed too, but its C++ status page still lists the directive as not yet
implemented for C++26, and with -pedantic it calls it a Clang extension even in C++26 mode. The numbers are the
character codes of the file: 78 is N, 97 is a, and so on.
Checkpoint
Check: #embed
Practise: one file for two libraries
Write a function print_numbered() that prints a list of strings as “1. idli”, “2. dosa” and so on, using
std::views::enumerate where the standard library has it and an ordinary index loop everywhere else. Then:
- Include
<version>before the check, and test both that__cpp_lib_ranges_enumerateis defined and that it is at least 202302L. - Make the decision once, in a macro of your own such as
USE_ENUMERATE, so the condition is not repeated. - Add a way to force the fallback, such as
-DFORCE_FALLBACK, so you can test the index loop on a library that hasenumerate. Untested fallback code is where portability bugs hide. - Build with both compilers if you have them, and check that the list is the same every time.
One possible solution, built three ways:
// numbered.cpp: one source file for two standard libraries. It uses std::views::enumerate
// where the library has it, and an index loop everywhere else.
// Build with -DFORCE_FALLBACK to test the index loop on a library that has enumerate.
#include <version>
#include <cstddef>
#include <iostream>
#include <string>
#include <vector>
// Decide once, give the decision a name, and test the name everywhere else.
#if defined(__cpp_lib_ranges_enumerate) && __cpp_lib_ranges_enumerate >= 202302L && !defined(FORCE_FALLBACK)
# define USE_ENUMERATE 1
# include <ranges>
#else
# define USE_ENUMERATE 0
#endif
void print_numbered(const std::vector<std::string>& items) {
#if USE_ENUMERATE
for (auto [i, item] : std::views::enumerate(items)) {
std::cout << i + 1 << ". " << item << '\n';
}
#else
for (std::size_t i = 0; i < items.size(); ++i) {
std::cout << i + 1 << ". " << items[i] << '\n';
}
#endif
}
int main() {
std::cout << "numbered with " << (USE_ENUMERATE ? "std::views::enumerate" : "an index loop") << '\n';
print_numbered({"idli", "dosa", "upma"});
} from session import run
# The same file, three builds: the same list each time, by two different routes.
run("""
clang++ -std=c++23 -Wall -Wextra numbered.cpp -o numbered && ./numbered
g++-16 -std=c++23 -Wall -Wextra numbered.cpp -o numbered && ./numbered
g++-16 -std=c++23 -Wall -Wextra -DFORCE_FALLBACK numbered.cpp -o numbered && ./numbered
""") Output
$ clang++ -std=c++23 -Wall -Wextra numbered.cpp -o numbered && ./numbered numbered with an index loop 1. idli 2. dosa 3. upma $ g++-16 -std=c++23 -Wall -Wextra numbered.cpp -o numbered && ./numbered numbered with std::views::enumerate 1. idli 2. dosa 3. upma $ g++-16 -std=c++23 -Wall -Wextra -DFORCE_FALLBACK numbered.cpp -o numbered && ./numbered numbered with an index loop 1. idli 2. dosa 3. upma
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 numbered_session.py
Clang took the index loop and GCC took enumerate, and -DFORCE_FALLBACK sent GCC down the index loop too: the same
three lines every time.
The practice below covers the seven ideas of this lesson and begins with the one you are least sure of. A wrong choice says what it misses:
Practice that adapts to you
Practise this lesson
17 questions about 7 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
Fresher · Why does #define SQ(x) x * x give the wrong answer for SQ(a + 1), and what would you write instead?
The preprocessor pastes the argument text into the replacement before the compiler applies any precedence rule, so
SQ(a + 1) becomes a + 1 * a + 1, which is 2a + 1, not (a + 1)². Parentheses around each use of the
parameter and around the whole body, ((x) * (x)), fix the precedence, but the argument is still evaluated twice, so
SQ(i++) is undefined behaviour and SQ(f()) calls f twice. The better answer is not a macro at all:
constexpr int sq(int x) { return x * x; } evaluates its argument once, checks its type, obeys scopes and
namespaces, and still works in constant expressions.
An interviewer who hears that answer may press on how you would prove it and how far the fix goes. Decide each follow-up, then read the note on every option:
Checkpoint
Interview round: two follow-up questions
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Mid to senior
Your library must build with GCC 16 and with Apple clang 21. A teammate used std::views::enumerate and std::move_only_function, and the macOS build broke. How do you keep one code base for both, and make sure the fallback paths are tested?
-
Mid to senior
A code base has 300 function-like macros, and you are asked to replace them with C++ functions. Which ones can be converted safely, which must stay macros, and what can change when a macro becomes a function?
Key takeaways
- The preprocessor rewrites text before compilation: it joins lines, drops comments, runs the directives in order and
expands macros, and
-Eshows the text the compiler gets. - A macro’s arguments are pasted as text, so parenthesise every use of a parameter and the whole body.
- A macro evaluates its argument once per use and ignores namespaces; prefer
constexprvariables and functions,inlinefunctions and templates, and keep macros for what only the preprocessor can do. #ifkeeps one branch and#and##build strings and names; feature-test macros, valued by year and month, say what a compiler and its library support.- Include
<version>before testing a library macro, prefer the macro to__has_includefor standard features, and give every fallback a way to be forced on. #embed(C++26) turns a file into array data; GCC 16 supports it as standard C++26, Clang 21 as an extension.
Terms in this lesson
- conditional compilation
- Keeping or dropping lines before compilation with #if, #ifdef, #elif and their relatives: only the first branch whose condition is true reaches the compiler, so the others may even use features one compiler lacks.
- macro
- A name defined with #define that the preprocessor replaces by its text wherever it appears later in the file, as SQ(a + 1) becomes ((a + 1) * (a + 1)). A macro has no type and no scope, and its arguments are pasted as text.
- preprocessor
- The first stage of a C++ build. It joins continued lines, replaces comments with spaces, runs the lines that start with #, such as #include and #define, and expands macros, so the compiler gets the rewritten text. -E shows that text.
References
- Working Draft, Programming Languages: C++ (N4950), 15 Preprocessing directives and 17.3 Implementation properties (ISO/IEC JTC1/SC22/WG21)
- SD-6: SG10 Feature Test Recommendations (Standard C++ Foundation)
- P1967R14: #embed, a simple, scannable preprocessor-based resource acquisition method (ISO/IEC JTC1/SC22/WG21)
- The C Preprocessor (GNU Project)
- The C Preprocessor: Initial processing (GNU Project)
- The C Preprocessor: Tokenization (GNU Project)
- The C Preprocessor: Include Operation (GNU Project)
- The C Preprocessor: Stringizing (GNU Project)
- The C Preprocessor: Standard Predefined Macros (GNU Project)
- C++ Standards Support in GCC (GNU Project)
- C++ Support in Clang (LLVM Project)
- libc++ C++23 Status (LLVM Project)
- The GNU C++ Library Manual: Status (GNU Project)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress