Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

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.

  • Intermediate
  • 20 minutes
  • Examples run with Clang 21.0.0, GCC 16.2.0 and Python 3.14.8
  • By MySmartCoPilot
Free lesson, Pro answers: 20 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

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: #define creates a name that is replaced by text, and #undef removes it.
  • Conditions: #if, #ifdef, #ifndef, #elif, #else and #endif, plus #elifdef and #elifndef since C++23, keep the lines of the first true branch and drop the rest.
  • Messages: #error stops the build with a message; #warning, standard since C++23, prints one and goes on.
  • Other: #pragma passes an instruction specific to the compiler, and #line changes 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:

Interactive · stage by stage What happens to square_macro.cpp before it is compiled Step through the stages in order. Each one works on the text the stage before it left, and an #include sends a whole header through them first.
What happens to square_macro.cpp before it is compiled, step 7 of 7. Showing Join continued lines, Replace comments, Split into tokens, Run the # lines in order, Expand the macros, Compile the unit. In focus: Compile the unit.#include: that file firstJoin continued linesjoined at a final \Replace commentseach becomes a single spaceSplit into tokensnames, numbers, punctuatorsRun the # lines in order#include, #define, #if …Expand the macrosreplaced by their textCompile the unitadjacent strings joined first

The drawing shows the whole scene; its 7 steps are listed below.

Every step as text
  1. 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.
  2. Comments. Every comment is replaced by a space, so none of them reaches the compiler, and the -E output of the session shows none.
  3. 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.
  4. #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.
  5. #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.
  6. 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.
  7. 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

  1. Question 1 of 2 Which statement about #warning is true?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: It prints a message and the build goes on, and it is standard since C++23

    The directives fall into five groups: files, macros, conditions, messages and others. #error and #warning are the messages: the first stops the build, the second only prints, and in C++20 mode Clang counts #warning as a C++23 extension.

  2. Question 2 of 2 square_macro.cpp starts with #include <iostream>. What does the preprocessor do with that line?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: It runs the whole of <iostream>, and every header it includes, through the same stages and pastes the result in place of the line

    An #include is replaced by the processed text of the named file: its comments become spaces, its directives run and its macros expand, recursively for the headers it includes, before the preprocessor goes on with the next line of the original file.

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 C++ · square_macro.cpp
// 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';
}
The output, and the text the compiler was given Python · square_session.py
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:

Interactive · step by step SQ_BAD(a + 1) and SQ(a + 1), pasted and evaluated Step through what the preprocessor pastes for each macro, then how the compiler groups the pasted text. a is 4.
SQ_BAD(a + 1) and SQ(a + 1), pasted and evaluated, step 5 of 5. Showing SQ_BAD(a + 1), a + 1 * a + 1, 9, SQ(a + 1), ((a + 1) * (a + 1)), 25. In focus: a + 1 * a + 1, ((a + 1) * (a + 1)).pastedevaluatedpastedevaluatedSQ_BAD(a + 1)#define SQ_BAD(x) x * xa + 1 * a + 1the text the compiler gets94 + (1 * 4) + 1SQ(a + 1)#define SQ(x) ((x) * (x))((a + 1) * (a + 1))the text the compiler gets255 * 5100 / SQ_BAD(5) is 100, not 4

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. 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.
  2. 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.
  3. 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)).
  4. 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.
  5. 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

  1. Question 1 of 2 With #define SQ_BAD(x) x * x and int a = 4;, what is the value of SQ_BAD(a + 2)?

    Type a number.

    A hint

    Write out the pasted text first, then apply precedence.

    Show the answer: Question 1 of 2

    Answer: 14

    The text becomes a + 2 * a + 2. Multiplication binds first, so it is 4 + (2 * 4) + 2 = 4 + 8 + 2 = 14, not the 36 the macro was meant to give.

  2. Question 2 of 2 #define SQ(x) (x) * (x) puts parentheses around each use of x but not around the body. What is 100 / SQ(5)?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: 100, because the text is 100 / (5) * (5)

    Parenthesising each parameter keeps an argument such as a + 1 together, but the body can still be split by an operator next to the call. Only ((x) * (x)) keeps both together, which is why 100 / SQ(5) printed 4.

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 C++ · twice.cpp
// 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 C++ · unsequenced.cpp
// 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++);
}
An argument evaluated twice Python · twice_session.py
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 C++ · max_macro.cpp
// 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
}
std::max after a max macro Python · max_session.py
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:

Interactive · answer and compare A macro, or a C++ feature? Say what the code you are about to write as a macro has to do. The table marks what fits and says why the rest does not.
What must the code do?

Answer the question above: the table then marks what fits your case and says why the rest does not.

A macro against four C++ features, on the four things that decide between them
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

  1. Question 1 of 2 twice.cpp calls next_id() once through SQ and once through sq(). How many times has next_id() run before the last line of main?

    Type a number.

    A hint

    SQ pastes its argument once for each use of x.

    Show the answer: Question 1 of 2

    Answer: 3 calls

    SQ(next_id()) becomes ((next_id()) * (next_id())), two calls that return 1 and 2. sq(next_id()) evaluates its argument once, 3. So next_id() ran 3 times, which is what the program printed.

  2. Question 2 of 2 Why does (std::max)(3, 7) compile after the max macro of max_macro.cpp, while std::max(3, 7) does not?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: A function-like macro expands only where its name is followed directly by (, and here a ) follows max

    The workaround relies on how function-like macros are recognised. The real fix is to remove the macro, or to #undef it right after the header that defines it.

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 C++ · directives.cpp
// 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__);
}
Both compilers, C++20 pedantic mode and -DNDEBUG Python · directives_session.py
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.

Interactive · step by step directives.cpp through Clang's preprocessor Step through the directives on the left in order, then the macros that main uses on the right. Each step says what the line becomes.
directives.cpp through Clang's preprocessor, step 8 of 8. Showing #include <cstdio>, Three #define lines, #if defined(__clang__) (COMPILER is "Clang"), #ifndef NDEBUG (kept: NDEBUG not defined), GLUE(order, _count) (becomes order_count), TO_TEXT(…) ("order_count * 2" = 6), LOG(…) ([log] built by Clang), __LINE__, __FILE__ (line 25 of directives.cpp). In focus: __LINE__, __FILE__.used in mainsets COMPILER#include <cstdio>declares std::printfThree #define linesTO_TEXT, GLUE and LOG#if defined(__clang__)COMPILER is "Clang"#ifndef NDEBUGkept: NDEBUG not definedGLUE(order, _count)becomes order_countTO_TEXT(…)"order_count * 2" = 6LOG(…)[log] built by Clang__LINE__, __FILE__line 25 of directives.cpp

The drawing shows the whole scene; its 8 steps are listed below.

Every step as text
  1. The header. #include <cstdio> pastes the declarations of the C library's printf, which main uses for every line it prints.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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.
  7. 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.
  8. 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", and GLUE(order, _count) became the variable name order_count.
  • LOG("built by " COMPILER) has no extra argument, so __VA_OPT__(,) left out the comma that would otherwise break the call to printf.
  • #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: #elifdef and #warning.
  • -DNDEBUG on the command line defines NDEBUG before the first line of the file, so the #warning branch is dropped. Many release builds define NDEBUG, which also turns every assert() 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

  1. Question 1 of 2 Which branch of directives.cpp does GCC 16 keep, and what does LOG print?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: #elifdef __GNUC__, so COMPILER is "GCC" and the program prints [log] built by GCC

    Each compiler took its own branch: Clang printed built by Clang, GCC built by GCC. The order of the tests matters, because Clang defines __GNUC__ as well.

  2. Question 2 of 2 The last build of directives.cpp added -DNDEBUG. What did that change in the text the compiler got?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: The #ifndef NDEBUG branch was dropped, so the #warning never ran

    A -D option defines a macro before the file is read, so it can switch any #if branch. Many release builds define NDEBUG, which also turns every assert() into nothing.

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 C++ · features.cpp
// 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);
}
Feature-test macros of the two toolchains Python · features_session.py
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:

Timeline · 7 events in order What __cplusplus says in each edition Walk through the editions. Each value is a year and a month, the same scheme the feature-test macros of this section use.
  1. 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.

  2. C++11

    201103L

    The value for the 2011 standard. #if __cplusplus >= 201103L is the classic test for it.

  3. C++14

    201402L

    The value for the 2014 standard.

  4. C++17

    201703L

    The value for the 2017 standard, which added __has_include, the test for a header that this lesson uses.

  5. 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.

  6. 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.

  7. 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

  1. Question 1 of 2 What does the value 202207L of __cpp_lib_print in libc++ tell you?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: The library has the revision of std::print that the value names by the year and month of its vote, and a later revision would have a larger value

    A feature-test macro is defined only when the feature is there, and its value names the revision. GCC 16's library reports 202406L for the same feature, a later revision.

  2. Question 2 of 2 Which of these macros can a file test before it includes any header?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: __cpp_explicit_this_parameter

    Language feature macros (__cpp_ followed by the feature) come from the compiler; library feature macros (__cpp_lib_) come from the standard library's headers, all of them from <version> since C++20.

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:

The same check, before and after <version>

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';
}
std::print, as each file sees it Python · version_session.py
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 C++ · header_or_feature.cpp
// 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 C++ · trust_header.cpp
// 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
Headers and feature macros, then a check that trusts the header Python · header_session.py
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:

Include <version>, then check that the feature macro is defined and at least the revision you need; otherwise use the fallback.#include <version>first__cpp_lib_Xdefined?Value at leastthe revisionyou need?Use thefeatureUse thefallbackNot __has_include alone:a header can existwhile its feature is offyesnoyesnowhy the macro

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.

  1. Include <version> at the top of the file. It defines the feature-test macros of the standard library.
  2. Ask whether __cpp_lib_X is defined. If it is not, use the fallback code.
  3. 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.
  4. 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

  1. Question 1 of 2 Which change makes too_early.cpp report that std::print is available on both libraries?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Move #include <version> above the #if

    A library macro exists only after a standard header has defined it. Put #include <version> first in every file that tests one, and the test sees what the library really supports.

  2. Question 2 of 2 On GCC 16 for macOS, __has_include(<stacktrace>) is true and __cpp_lib_stacktrace is not defined. Which test should portable code use before it calls std::stacktrace::current()?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: #if defined(__cpp_lib_stacktrace), after #include <version>

    Use the feature-test macro, after <version>, for everything the standard defines, and keep __has_include for headers outside the standard library, such as an optional third-party library.

#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 C++ · embed_demo.cpp
// 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));
}
Seven bytes of greeting.txt, three builds Python · embed_session.py
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:

Interactive · step by step Seven bytes of greeting.txt become an array Step from the file on disk to the numbers the program prints. Watch where the bytes go at each stage.
Seven bytes of greeting.txt become an array, step 5 of 5. Showing greeting.txt (7 bytes on disk), #embed "greeting.txt", greeting[7] (78, 97, 109, 97, 115, …), ./embed (as text: Namaste). In focus: #embed "greeting.txt".read at build timebecomes numbersprintedgreeting.txt7 bytes on disk#embed "greeting.txt"between the bracesgreeting[7]78, 97, 109, 97, 115, …./embedas text: NamasteGCC 16: C++26; Clang 21: an extension

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. The file. greeting.txt holds the seven characters of Namaste and nothing else: no newline at the end, so wc -c counts 7 bytes.
  2. 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.
  3. 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.
  4. 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.
  5. 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.

Hex Viewer & File Inspector Open any small file to see the byte values that #embed would write into your array.

Checkpoint

Check: #embed

  1. Question 1 of 2 greeting.txt holds 7 bytes. How many elements does constexpr unsigned char greeting[] = { #embed "greeting.txt" }; have?

    Type a number.

    Show the answer: Question 1 of 2

    Answer: 7 elements

    #embed writes one number for each byte of the file, so the array has 7 elements, and sizeof greeting printed 7.

  2. Question 2 of 2 A code base builds with -pedantic -Werror on GCC 16 in C++26 mode and on Clang 21 in C++26 mode. Can it use #embed?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Not on Clang 21: with -pedantic it calls #embed a Clang extension even in C++26 mode, and -Werror makes that warning an error

    GCC has implemented the C++26 directive since GCC 15, and Clang 21 accepts it as an extension. A build that turns warnings into errors can keep a generated array for Clang, or switch off the one warning the message names, -Wc23-extensions.

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:

  1. Include <version> before the check, and test both that __cpp_lib_ranges_enumerate is defined and that it is at least 202302L.
  2. Make the decision once, in a macro of your own such as USE_ENUMERATE, so the condition is not repeated.
  3. Add a way to force the fallback, such as -DFORCE_FALLBACK, so you can test the index loop on a library that has enumerate. Untested fallback code is where portability bugs hide.
  4. 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 C++ · numbered.cpp
// 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"});
}
Two routes to the same list Python · numbered_session.py
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.

Text Diff Save the output of clang++ -E -P and g++-16 -E -P for numbered.cpp and compare which branch each compiler kept.

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.

  1. Question 1 of 17 Which option stops a GCC or Clang build after the preprocessor and prints the result?

    Choose one answer.

    Show the answer: Question 1 of 17

    Answer: -E

    clang++ -E -P file.cpp prints the translation unit without line markers, which is how the square session showed the five std::cout lines.

  2. Question 2 of 17 What does the compiler see for the second line?

    Read the code, then choose one answer.

    #define LIMIT 10 // per user
    int n = LIMIT * 2;
    Show the answer: Question 2 of 17

    Answer: int n = 10 * 2;

    The preprocessor replaces every comment with a space before it splits the text into tokens and runs the directives, so the replacement of LIMIT is the single token 10, and n is 20.

  3. Question 3 of 17 With #define SQ_BAD(x) x * x and int a = 4;, what is the value of SQ_BAD(a + 1)?

    Type a number.

    Show the answer: Question 3 of 17

    Answer: 9

    The preprocessor pastes the text, giving a + 1 * a + 1. Multiplication binds first, so it is 4 + 4 + 1 = 9, as the square session printed. With ((x) * (x)) the expansion is ((a + 1) * (a + 1)), which is 25.

  4. Question 4 of 17 With #define CUBE(x) x * x * x, what is the value of CUBE(2 + 1)?

    Type a number.

    A hint

    Paste 2 + 1 for each x, then multiply before you add.

    Show the answer: Question 4 of 17

    Answer: 7

    The text is 2 + 1 * 2 + 1 * 2 + 1. Each 1 * 2 is 2, so it is 2 + 2 + 2 + 1 = 7, not 27. ((x) * (x) * (x)) would give 27.

  5. Question 5 of 17 Which definition of HALF gives 5 for HALF(4 + 6) and 2 for 10 / HALF(10)?

    Choose one answer.

    Show the answer: Question 5 of 17

    Answer: #define HALF(x) ((x) / 2)

    Both pairs of parentheses are needed: one around each use of the parameter, so an argument stays together, and one around the whole body, so an operator next to the call cannot split it.

  6. Question 6 of 17 A header defines #define max(a, b) ((a) > (b) ? (a) : (b)). Which calls to the standard function still compile after it?

    Choose every answer that is right.

    Show the answer: Question 6 of 17

    Answer:

    • (std::max)(3, 7)
    • std::max<int>(3, 7)

    A function-like macro is expanded only where its name is directly followed by an opening parenthesis. In (std::max)(3, 7) the name is followed by a closing parenthesis, and in std::max<int>(3, 7) by a less-than sign, so the macro leaves both alone, and both print 7. In std::max(3, 7) and ::std::max(3, 7) the name is followed by ( and is rewritten into std::((3) > (7) ? (3) : (7)), which does not compile.

  7. Question 7 of 17 In twice.cpp, why does SQ(next_id()) give 2 while sq(next_id()) gives 9?

    Choose one answer.

    Show the answer: Question 7 of 17

    Answer: The macro pastes next_id() twice, so it runs twice and multiplies 1 by 2; the function evaluates its argument once, 3, and squares it

    A macro's argument is evaluated once for every use of the parameter in the body, which matters as soon as the argument has a side effect, as a call of next_id() does.

  8. Question 8 of 17 With #define SQ(x) ((x) * (x)), what does return SQ(i++); do?

    Choose one answer.

    Show the answer: Question 8 of 17

    Answer: It is undefined behaviour, because i is changed twice in one expression with no order between the changes

    Two unsequenced changes of the same variable in one expression are undefined behaviour. A constexpr function, sq(i++), changes i once, before the call.

  9. Question 9 of 17 What does this print?

    Read the code, then choose one answer.

    #define TO_TEXT(x) #x
    std::puts(TO_TEXT(a + b));
    Show the answer: Question 9 of 17

    Answer: a + b

    Stringizing is how TO_TEXT(order_count * 2) printed the text order_count * 2 before its value in directives.cpp.

  10. Question 10 of 17 With #define GLUE(a, b) a##b, what does int GLUE(total, _paise) = 0; declare?

    Choose one answer.

    Show the answer: Question 10 of 17

    Answer: An int named total_paise

    Token pasting builds names that appear nowhere in the source, which is powerful and also why such code is hard to search: grep finds no total_paise.

  11. Question 11 of 17 What does the <version> header declare?

    Choose one answer.

    Show the answer: Question 11 of 17

    Answer: No functions or types, only macros, among them every library feature-test macro

    Since C++20, <version> collects all library feature-test macros in one header that declares nothing else.

  12. Question 12 of 17 Code needs the 202406L revision of std::print. Which libraries of the session pass #if __cpp_lib_print >= 202406L?

    Choose one answer.

    Show the answer: Question 12 of 17

    Answer: Only GCC 16's libstdc++, which reports 202406L

    A test of the form >= value asks for that revision or a later one. The table shows 202207L for libc++ and 202406L for libstdc++ in both modes.

  13. Question 13 of 17 A colleague writes #if __cplusplus >= 202302L to decide whether std::views::enumerate exists. What happens on Apple clang 21 in C++23 mode?

    Choose one answer.

    Show the answer: Question 13 of 17

    Answer: The test is true, but libc++ has no views::enumerate, so the code fails to compile; test __cpp_lib_ranges_enumerate instead

    A compiler can support an edition in part, and its library can lag behind. Test the macro of the feature you need, after <version>, as numbered.cpp does.

  14. Question 14 of 17 A file tests #if defined(__cpp_lib_print) on its first line and includes <iostream> after it. On a library that has std::print, what does the test see?

    Choose one answer.

    Show the answer: Question 14 of 17

    Answer: The macro is not defined yet, so the test takes the "not available" branch.

    Library feature-test macros are defined by <version> and by the header of each feature, so a test above every #include sees nothing and silently takes the fallback. Only the language macros, such as __cpp_explicit_this_parameter, are predefined by the compiler.

  15. Question 15 of 17 On GCC 16 for macOS, __has_include(<stacktrace>) is true but __cpp_lib_stacktrace is not defined. What should portable code conclude?

    Choose one answer.

    Show the answer: Question 15 of 17

    Answer: The header exists, but std::stacktrace is not available in this build, so use the fallback.

    __has_include only says the file can be found. libstdc++ wraps the contents of <stacktrace> in a check of __cpp_lib_stacktrace, which this build does not define, so including the header declares nothing. The feature macro is the test that matches what the code can use.

  16. Question 16 of 17 What does #embed "greeting.txt" put between the braces of an array's initialiser?

    Choose one answer.

    Show the answer: Question 16 of 17

    Answer: The file's bytes, as a comma-separated list of numbers

    #embed replaces the old extra build step that converted a file into a generated array: the preprocessor writes the array's initialiser itself.

  17. Question 17 of 17 Which builds of embed_demo.cpp printed a warning in the "Seven bytes" session?

    Choose one answer.

    Show the answer: Question 17 of 17

    Answer: GCC in C++23 mode with -pedantic, and Clang in C++26 mode with -pedantic.

    For GCC 16, #embed is standard C++26, so only the C++23 build was flagged as an extension. Clang 21 accepts #embed but treats it as its own extension in C++ modes, so -pedantic flags it even in C++26 mode.

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

  1. Question 1 of 2 The interviewer asks: how would you prove what SQ(a + 1) becomes, rather than just claim it?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Show the preprocessor's output: clang++ -E -P square_macro.cpp prints the line with a + 1 * a + 1

    -E (with -P to drop line markers) is the direct evidence for any question about macros: it is the translation unit as the compiler receives it.

  2. Question 2 of 2 Then: your constexpr int sq(int x) is fine, but the code also squares doubles. What do you write instead of a macro?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: A function template, such as template <typename T> constexpr T sq(T x) { return x * x; }

    For code that works on many types, templates do what a macro does without its problems: they are real functions, checked for each type they are used with.

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. 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?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. 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?

    Model answer, Pro. First step and follow-ups

Key takeaways

  • The preprocessor rewrites text before compilation: it joins lines, drops comments, runs the directives in order and expands macros, and -E shows 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 constexpr variables and functions, inline functions and templates, and keep macros for what only the preprocessor can do.
  • #if keeps 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_include for 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

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.