Skip to content

/flags/cpp/gpp-e-preprocess-only

g++ -E: show the preprocessed C++ source (debug macros and #include)

verified onenvironmentlocal · windows 10.0.26200 arm64 · GNU bash 5.3.15 (Git Bash)updated
tool
C++
command
g++
flag
-E
default
off: preprocessing is followed by compilation and linking

g++ -E file.cpp runs only the preprocessor and writes the result to standard output: every #include is replaced by the header's text, macros are expanded, #if blocks that are false are removed, and comments disappear. Nothing is compiled. It is the tool for understanding why a macro misbehaves, which header defines something, or which branch of an #ifdef was taken. Add -P to drop the line markers and -o file.ii to save the output.

Macro expansion made visible

The example uses Clang 20 from the Zig 0.15.1 toolchain (installed from PyPI with uv); cxx below is that compiler, and g++ accepts the same options:

bash
cxx() { uv run -q --no-project --python 3.13 --with ziglang==0.15.1 python -m ziglang c++ -fno-color-diagnostics "$@"; }
cat > macros.cpp <<'EOF'
#define SQUARE(x) x * x
#define SQUARE_OK(x) ((x) * (x))
#define NAME "errcodex"
#ifdef FAST
int mode = 1;   // fast path
#else
int mode = 0;   // default path
#endif
int a = SQUARE(1 + 2);
int b = SQUARE_OK(1 + 2);
const char* n = NAME;
EOF
echo "== g++ -E -P"
cxx -E -P macros.cpp
echo "== g++ -E -P -DFAST"
cxx -E -P -DFAST macros.cpp | grep mode
echo "== line markers without -P"
cxx -E macros.cpp | grep -m1 '^# 1 "macros.cpp"'
echo "== <iostream> expands to"
printf '#include <iostream>\n' > io.cpp
lines=$(cxx -std=c++17 -E -P io.cpp | wc -l)
[ "$lines" -gt 10000 ] && echo "more than 10000 lines"

Output

== g++ -E -P
int mode = 0;
int a = 1 + 2 * 1 + 2;
int b = ((1 + 2) * (1 + 2));
const char* n = "errcodex";
== g++ -E -P -DFAST
int mode = 1;
== line markers without -P
# 1 "macros.cpp"
== <iostream> expands to
more than 10000 lines

SQUARE(1 + 2) expands to 1 + 2 * 1 + 2, which is 5, not 9: the classic missing-parentheses bug, plainly visible in the -E output. The #ifdef branch changes with -DFAST, comments are gone, and a single #include <iostream> becomes tens of thousands of lines.

Useful combinations

CommandShows
g++ -E file.cppPreprocessed source with line markers
g++ -E -P file.cppSame without line markers, easier to read
g++ -E file.cpp -o file.iiSave it; .ii is the conventional extension for preprocessed C++
g++ -dM -E file.cppAll macros defined at the end, instead of the code
g++ -dM -E -x c++ /dev/nullPredefined macros of the compiler (__cplusplus, __GNUC__...)
g++ -E -H file.cppAlso print the tree of included headers on stderr
g++ -M / -MM file.cppMake dependency rules (all headers / non-system headers)
g++ -E -C file.cppKeep comments

When -E helps

  • "was not declared" after a macro: if a name disappears or turns into something else, a macro with the same name is the usual cause. Look at the expanded line; see was not declared in this scope.
  • Which header provides a declaration: g++ -E -H lists the include tree, and the line markers in -E output show the file where a declaration came from. Missing standard headers often cause no member named X in namespace std.
  • Platform #ifdefs: check which branch is active for a given target or -D set without building.
  • Bug reports: compiler developers ask for a preprocessed file (-E, or -save-temps), because it reproduces the problem without your include paths.

Pitfalls

  • The output depends on every flag that affects the preprocessor: -std, -D, -I, the target. Use the same flags as the real build, or the result can differ.
  • -E does not check that the code is valid C++; syntax errors appear only when compiling.
  • The __cplusplus value changes with -std (for example 201703L for C++17), which switches code paths in headers; see -std=c++17.

FAQ

Why is the -E output of a small file so long?

Every #include is expanded recursively, so including <iostream> or <string> pulls in tens of thousands of lines of the standard library. Pipe the output through tail, grep for your own identifiers, or preprocess a file without system headers.

What are the lines starting with # in the output?

Line markers such as # 3 "main.cpp" tell the compiler which file and line the following code came from, so error messages point to the original source. -P removes them when you only want to read the code.

How do I see the value of a macro?

g++ -dM -E file.cpp lists all macros defined at the end of preprocessing, including predefined ones; grep for the name. For a single expression, put it in a small file and run g++ -E -P on it.

Sources