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 1 – Start here: toolchains, compiling and first programs

Compiler flags every C++ learner should use

Turn on the GCC, Clang and MSVC warnings that catch real bugs, see which flag reports each of five mistakes, and pick -O0 -g or -O2 for each build.

  • Beginner
  • 15 minutes
  • Examples run with Clang 21.0.0, GCC 16.2.0 and Python 3.14.8
  • By MySmartCoPilot
Free: 15 minutes

What you will learn

  • Use the warning flags that catch real bugs with GCC and Clang
  • Choose optimisation and debug flags for development and for timing
  • Apply equivalent settings in MSVC, GCC and Clang

Before you start

A C++ compiler accepts many programs that are wrong. By default it reports only a few problems, so a missing return, a comparison that is false when it looks true, or a fraction silently thrown away can all compile and run. Warning flags make the compiler report questionable code; optimisation and debug flags decide how fast the program runs and how easily a debugger can follow it. For this track the short answer is: build with -std=c++23 -Wall -Wextra -Wpedantic every time, add -g while you write and debug, time only -O2 builds, and use -Werror where a warning must stop the build. With Microsoft’s compiler the same habits are /std:c++latest /W4 /permissive- /EHsc. The rest of this lesson shows what each flag catches, on a program with five real mistakes, compiled with Apple clang 21 and GCC 16.

The flags at a glance

Flag What it does When to use it
-std=c++23 Selects the language version Always. Without it Apple clang 21 compiles GNU C++14 and GCC 16 GNU C++20
-Wall Turns on a set of common warnings (not all of them) Always
-Wextra Adds warnings that -Wall leaves out Always
-Wpedantic Warns about compiler extensions that standard C++ forbids Always, so the code also builds elsewhere
-Wconversion, -Wshadow Report lossy conversions and names that hide other names Often; they are noisier
-Werror Turns every warning into an error In CI, for work you hand in, and in contests if you like
-O0, -O2 No optimisation (the default) or most optimisations -O0 while debugging; -O2 for timing and release
-g Stores names and line numbers for a debugger While writing and debugging; it works with any -O level
-fsanitize=address,undefined Adds run-time checks for memory errors and undefined behaviour While hunting a bug; the program runs slower

The defaults in that first row are easy to check: this program prints __cplusplus, the number that names the standard a build uses.

standard_version.cpp C++ · standard_version.cpp
#include <iostream>

int main() {
    std::cout << __cplusplus << '\n';
}
With and without -std Python · defaults_session.py
from session import run

# The C++ standard each compiler uses with and without -std, as the value of __cplusplus.
run("""
clang++ standard_version.cpp -o standard_version && ./standard_version
clang++ -std=c++23 standard_version.cpp -o standard_version && ./standard_version
g++-16 standard_version.cpp -o standard_version && ./standard_version
g++-16 -std=c++23 standard_version.cpp -o standard_version && ./standard_version
""")

Output

$ clang++ standard_version.cpp -o standard_version && ./standard_version
201402
$ clang++ -std=c++23 standard_version.cpp -o standard_version && ./standard_version
202302
$ g++-16 standard_version.cpp -o standard_version && ./standard_version
202002
$ g++-16 -std=c++23 standard_version.cpp -o standard_version && ./standard_version
202302

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 defaults_session.py

Without -std, Apple clang 21 printed 201402 (C++14) and GCC 16 printed 202002 (C++20); GCC’s release notes say it moved its default from gnu++17 to gnu++20. Both defaults are GNU dialects, which accept extensions. -std=c++23 gave 202302 on both: standard C++23.

One set of warning flags used in every build, and four jobs that each add their own flags: -g, sanitizers, -O2 or -Werror.Every build-std=c++23-Wall -Wextra -WpedanticAdd for the jobWriting and learning-gHunting a bug-O0 -g-fsanitize=address,undefinedTiming, contestsand release-O2CI and workyou hand in-Werror

The same warnings in every build, plus what each job needs

Text description of the diagram

The top box is the base that every build uses: -std=c++23 with -Wall, -Wextra and -Wpedantic. An arrow leads from it to four boxes, each adding flags for one job.

  1. Writing and learning: add -g, so a debugger can show names and line numbers.
  2. Hunting a bug: add -O0 -g and -fsanitize=address,undefined, which check memory use and undefined behaviour while the program runs.
  3. Timing, contests and release: add -O2, the optimised build to time and ship.
  4. CI and work you hand in: add -Werror, so any warning stops the build.

Five mistakes that compile

This program grades three marks and prints their average. It compiles, runs and prints believable numbers, yet it holds five mistakes:

A program with five mistakes C++ · five_bugs.cpp
#include <iostream>
#include <vector>

int grade_points(int mark) {
    if (mark >= 75) return 10;
    if (mark >= 50) return 7;
}

int main() {
    std::vector<int> marks{82, 57, 69};
    int count = 0;
    double total = 0;
    for (int i = 0; i < marks.size(); ++i) {
        total += marks[i];
    }
    int average = total / marks.size();
    for (int mark : marks) {
        int average = grade_points(mark);
        std::cout << mark << " -> " << average << " points\n";
    }
    std::cout << "Average mark: " << average << '\n';
}

Each session below builds and runs it once, then compiles it again with more and more warning flags, keeping one line per warning:

Five mistakes, four sets of flags

Clang 21 · five_bugs_clang.py

from session import run

# five_bugs.cpp with more and more warning flags, with Clang 21. -c compiles without linking, and grep keeps one line
# per warning.
run("""
clang++ -std=c++23 five_bugs.cpp -o five_bugs && ./five_bugs
clang++ -std=c++23 -c five_bugs.cpp -o five_bugs.o 2>&1 | grep warning:
clang++ -std=c++23 -Wall -c five_bugs.cpp -o five_bugs.o 2>&1 | grep warning:
clang++ -std=c++23 -Wall -Wextra -c five_bugs.cpp -o five_bugs.o 2>&1 | grep warning:
clang++ -std=c++23 -Wall -Wextra -Wpedantic -Wconversion -Wshadow -c five_bugs.cpp -o five_bugs.o 2>&1 | grep warning:
""")

Output

$ clang++ -std=c++23 five_bugs.cpp -o five_bugs && ./five_bugs
five_bugs.cpp:7:1: warning: non-void function does not return a value in all control paths [-Wreturn-type]
    7 | }
      | ^
1 warning generated.
82 -> 10 points
57 -> 7 points
69 -> 7 points
Average mark: 69
$ clang++ -std=c++23 -c five_bugs.cpp -o five_bugs.o 2>&1 | grep warning:
five_bugs.cpp:7:1: warning: non-void function does not return a value in all control paths [-Wreturn-type]
$ clang++ -std=c++23 -Wall -c five_bugs.cpp -o five_bugs.o 2>&1 | grep warning:
five_bugs.cpp:7:1: warning: non-void function does not return a value in all control paths [-Wreturn-type]
five_bugs.cpp:11:9: warning: unused variable 'count' [-Wunused-variable]
$ clang++ -std=c++23 -Wall -Wextra -c five_bugs.cpp -o five_bugs.o 2>&1 | grep warning:
five_bugs.cpp:7:1: warning: non-void function does not return a value in all control paths [-Wreturn-type]
five_bugs.cpp:11:9: warning: unused variable 'count' [-Wunused-variable]
five_bugs.cpp:13:23: warning: comparison of integers of different signs: 'int' and 'size_type' (aka 'unsigned long') [-Wsign-compare]
$ clang++ -std=c++23 -Wall -Wextra -Wpedantic -Wconversion -Wshadow -c five_bugs.cpp -o five_bugs.o 2>&1 | grep warning:
five_bugs.cpp:7:1: warning: non-void function does not return a value in all control paths [-Wreturn-type]
five_bugs.cpp:14:24: warning: implicit conversion changes signedness: 'int' to 'size_type' (aka 'unsigned long') [-Wsign-conversion]
five_bugs.cpp:16:25: warning: implicit conversion turns floating-point number into integer: 'double' to 'int' [-Wfloat-conversion]
five_bugs.cpp:16:33: warning: implicit conversion from 'size_type' (aka 'unsigned long') to 'double' may lose precision [-Wimplicit-int-float-conversion]
five_bugs.cpp:18:13: warning: declaration shadows a local variable [-Wshadow]
five_bugs.cpp:11:9: warning: unused variable 'count' [-Wunused-variable]
five_bugs.cpp:13:23: warning: comparison of integers of different signs: 'int' and 'size_type' (aka 'unsigned long') [-Wsign-compare]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 five_bugs_clang.py

GCC 16 · five_bugs_gcc.py

from session import run

# five_bugs.cpp with more and more warning flags, with GCC 16. -c compiles without linking, and grep keeps one line
# per warning.
run("""
g++-16 -std=c++23 five_bugs.cpp -o five_bugs && ./five_bugs
g++-16 -std=c++23 -c five_bugs.cpp -o five_bugs.o 2>&1 | grep warning:
g++-16 -std=c++23 -Wall -c five_bugs.cpp -o five_bugs.o 2>&1 | grep warning:
g++-16 -std=c++23 -Wall -Wextra -c five_bugs.cpp -o five_bugs.o 2>&1 | grep warning:
g++-16 -std=c++23 -Wall -Wextra -Wpedantic -Wconversion -Wshadow -c five_bugs.cpp -o five_bugs.o 2>&1 | grep warning:
""")

Output

$ g++-16 -std=c++23 five_bugs.cpp -o five_bugs && ./five_bugs
five_bugs.cpp: In function 'int grade_points(int)':
five_bugs.cpp:7:1: warning: control reaches end of non-void function [-Wreturn-type]
    7 | }
      | ^
82 -> 10 points
57 -> 7 points
69 -> 7 points
Average mark: 69
$ g++-16 -std=c++23 -c five_bugs.cpp -o five_bugs.o 2>&1 | grep warning:
five_bugs.cpp:7:1: warning: control reaches end of non-void function [-Wreturn-type]
$ g++-16 -std=c++23 -Wall -c five_bugs.cpp -o five_bugs.o 2>&1 | grep warning:
five_bugs.cpp:13:23: warning: comparison of integer expressions of different signedness: 'int' and 'std::vector<int>::size_type' {aka 'long unsigned int'} [-Wsign-compare]
five_bugs.cpp:11:9: warning: unused variable 'count' [-Wunused-variable]
five_bugs.cpp:7:1: warning: control reaches end of non-void function [-Wreturn-type]
$ g++-16 -std=c++23 -Wall -Wextra -c five_bugs.cpp -o five_bugs.o 2>&1 | grep warning:
five_bugs.cpp:13:23: warning: comparison of integer expressions of different signedness: 'int' and 'std::vector<int>::size_type' {aka 'long unsigned int'} [-Wsign-compare]
five_bugs.cpp:11:9: warning: unused variable 'count' [-Wunused-variable]
five_bugs.cpp:7:1: warning: control reaches end of non-void function [-Wreturn-type]
$ g++-16 -std=c++23 -Wall -Wextra -Wpedantic -Wconversion -Wshadow -c five_bugs.cpp -o five_bugs.o 2>&1 | grep warning:
five_bugs.cpp:13:23: warning: comparison of integer expressions of different signedness: 'int' and 'std::vector<int>::size_type' {aka 'long unsigned int'} [-Wsign-compare]
five_bugs.cpp:16:37: warning: conversion from 'std::vector<int>::size_type' {aka 'long unsigned int'} to 'double' may change value [-Wconversion]
five_bugs.cpp:16:25: warning: conversion from 'double' to 'int' may change value [-Wfloat-conversion]
five_bugs.cpp:18:13: warning: declaration of 'average' shadows a previous local [-Wshadow]
five_bugs.cpp:11:9: warning: unused variable 'count' [-Wunused-variable]
five_bugs.cpp:7:1: warning: control reaches end of non-void function [-Wreturn-type]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 five_bugs_gcc.py

Both builds printed 82 -> 10 points, 57 -> 7 points, 69 -> 7 points and Average mark: 69, though the real average is 69.33. Here is which flag first made each compiler report each mistake:

Mistake Line Clang 21 reports it with GCC 16 reports it with
grade_points has no return for marks under 50 7 no flag: on by default no flag: on by default
count is never used 11 -Wall -Wall
int i compared with marks.size(), which is unsigned 13 -Wextra -Wall
total / marks.size() stored in an int, dropping .33 16 -Wconversion -Wconversion
The inner average hides the outer one 18 -Wshadow -Wshadow

So -Wall alone found two mistakes on Clang and three on GCC, and only the full set -Wall -Wextra -Wpedantic -Wconversion -Wshadow found all five on both. Each warning ends with the flag that controls it, which is also how you switch that one warning off. Clang wrote comparison of integers of different signs: 'int' and 'size_type' (aka 'unsigned long') [-Wsign-compare] and GCC wrote comparison of integer expressions of different signedness: 'int' and 'std::vector<int>::size_type' {aka 'long unsigned int'} [-Wsign-compare]: different words, the same flag. With the full set Clang printed 7 warnings and GCC 6, because -Wconversion also reports conversions that are harmless here, such as a vector’s size turned into a double. That noise is why these two flags are left out of -Wall and -Wextra.

The two compilers do not group warnings the same way: GCC’s manual lists -Wsign-compare under -Wall for C++, while Clang’s reference lists it under -Wextra. Building with both compilers, as this track does, catches more than either one alone.

Why a signed and unsigned comparison is a real bug

std::vector::size() returns an unsigned type. When an int meets it in a comparison, the int is converted to that unsigned type first, and a negative value becomes a huge one:

When -1 is not less than 3 C++ · sign_compare.cpp
#include <iostream>
#include <vector>

int main() {
    const std::vector<int> scores{40, 55, 61};
    int index = -1;

    if (index < scores.size()) {
        std::cout << "-1 is less than 3\n";
    } else {
        std::cout << "-1 is not less than 3\n";
    }
    std::cout << "-1 converted to size_t: " << static_cast<std::size_t>(index) << '\n';
}
Built with -Wall -Wextra, then run Python · sign_compare_session.py
from session import run

run("""
clang++ -std=c++23 -Wall -Wextra sign_compare.cpp -o sign_compare && ./sign_compare
g++-16 -std=c++23 -Wall -Wextra sign_compare.cpp -o sign_compare && ./sign_compare
""")

Output

$ clang++ -std=c++23 -Wall -Wextra sign_compare.cpp -o sign_compare && ./sign_compare
sign_compare.cpp:8:15: warning: comparison of integers of different signs: 'int' and 'size_type' (aka 'unsigned long') [-Wsign-compare]
    8 |     if (index < scores.size()) {
      |         ~~~~~ ^ ~~~~~~~~~~~~~
1 warning generated.
-1 is not less than 3
-1 converted to size_t: 18446744073709551615
$ g++-16 -std=c++23 -Wall -Wextra sign_compare.cpp -o sign_compare && ./sign_compare
sign_compare.cpp: In function 'int main()':
sign_compare.cpp:8:15: warning: comparison of integer expressions of different signedness: 'int' and 'std::vector<int>::size_type' {aka 'long unsigned int'} [-Wsign-compare]
    8 |     if (index < scores.size()) {
      |         ~~~~~~^~~~~~~~~~~~~~~
-1 is not less than 3
-1 converted to size_t: 18446744073709551615

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 sign_compare_session.py

Both compilers warned with -Wall -Wextra, and the program printed -1 is not less than 3, then -1 converted to size_t: 18446744073709551615, which is 2⁶⁴ − 1. A loop that counts down past zero, or an index of -1 meaning “not found”, falls into this. Change the index to std::size_t, or loop with a range-based for that has no index at all.

Programmer Calculator (Bitwise) Type -1 as a 64-bit signed number and read it as unsigned to see 18446744073709551615.

One missing return, four different results

The first mistake deserves a closer look, because it is the most dangerous. GCC’s manual is explicit: in C++, when a function that returns a value can run off its end (main excepted), the behaviour is undefined, whether or not anyone uses the result. The compiler may then produce anything. This program asks for the points of a mark of 47:

Asking for a value the function never returns C++ · missing_return.cpp
#include <iostream>

int grade_points(int mark) {
    if (mark >= 75) return 10;
    if (mark >= 50) return 7;
}

int main() {
    for (int mark : {82, 57, 47}) {
        std::cout << mark << " -> " << grade_points(mark) << " points\n";
    }
}
Four builds and two checked builds Python · missing_return_session.py
from session import run

# One file, four builds and two checked builds. -Wno-return-type hides the warning that every build would repeat.
run("""
clang++ -std=c++23 -Wno-return-type -O0 missing_return.cpp -o missing_return
./missing_return
clang++ -std=c++23 -Wno-return-type -O2 missing_return.cpp -o missing_return
./missing_return
g++-16 -std=c++23 -Wno-return-type -O0 missing_return.cpp -o missing_return
./missing_return
g++-16 -std=c++23 -Wno-return-type -O2 missing_return.cpp -o missing_return
./missing_return
clang++ -std=c++23 -Wno-return-type -g -fsanitize=address,undefined missing_return.cpp -o missing_return
./missing_return
g++-16 -std=c++23 -Wno-return-type -g -fsanitize=address,undefined missing_return.cpp -o missing_return
./missing_return
""")

Output

$ clang++ -std=c++23 -Wno-return-type -O0 missing_return.cpp -o missing_return
$ ./missing_return
82 -> 10 points
57 -> 7 points
47 -> 1 points
$ clang++ -std=c++23 -Wno-return-type -O2 missing_return.cpp -o missing_return
$ ./missing_return
82 -> 10 points
57 -> 7 points
47 -> 7 points
$ g++-16 -std=c++23 -Wno-return-type -O0 missing_return.cpp -o missing_return
$ ./missing_return
[exit status 133]
$ g++-16 -std=c++23 -Wno-return-type -O2 missing_return.cpp -o missing_return
$ ./missing_return
82 -> 10 points
57 -> 7 points
47 -> 7 points
$ clang++ -std=c++23 -Wno-return-type -g -fsanitize=address,undefined missing_return.cpp -o missing_return
$ ./missing_return
missing_return.cpp:3:5: runtime error: execution reached the end of a value-returning function without returning a value
SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior missing_return.cpp:3:5 
82 -> 10 points
57 -> 7 points
47 -> 
[exit status 134]
$ g++-16 -std=c++23 -Wno-return-type -g -fsanitize=address,undefined missing_return.cpp -o missing_return
$ ./missing_return
missing_return.cpp:3:5: runtime error: execution reached the end of a value-returning function without returning a value
82 -> 10 points
57 -> 7 points
47 -> 
[exit status 134]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 missing_return_session.py

The same file gave four different results on the same computer:

Build What the program did for 47
clang++ -std=c++23 -O0 Printed 47 -> 1 points
clang++ -std=c++23 -O2 Printed 47 -> 7 points
g++-16 -std=c++23 -O0 Stopped with a trap (exit status 133 on macOS)
g++-16 -std=c++23 -O2 Printed 47 -> 7 points

None of them is right, and the optimised builds make the mistake look like a plausible answer. The GCC -O0 build stopped because GCC’s -funreachable-traps, on by default at -O0 and -Og, turns code the compiler treats as unreachable into a trap instead of running past it. In the recorded session, whose output went to a pipe, that build printed nothing at all: the first two lines were still waiting in the output buffer when the program stopped. In a terminal you would see them.

Run-time checks find this kind of mistake while the program runs. Built with -g -fsanitize=address,undefined, the Clang and GCC builds both stopped at the bad call (exit status 134) and reported missing_return.cpp:3:5: runtime error: execution reached the end of a value-returning function without returning a value. A later lesson covers sanitizers in full; for now, add -Werror=return-type to every build, so this one warning always stops the compile.

-Wpedantic: when the compiler’s own extensions slip in

Compilers accept a few things that standard C++ does not have. A common one is an array whose size is only known while the program runs:

A variable-length array C++ · vla.cpp
#include <iostream>

int main() {
    int n = 0;
    std::cin >> n;
    int squares[n];
    for (int i = 0; i < n; ++i) {
        squares[i] = i * i;
    }
    std::cout << "Last square: " << squares[n - 1] << '\n';
}
Which builds notice the extension Python · vla_session.py
from session import run

run("""
g++-16 -std=c++23 -Wall -Wextra vla.cpp -o vla && echo 5 | ./vla
g++-16 -std=c++23 -Wall -Wextra -Wpedantic vla.cpp -o vla
clang++ vla.cpp -o vla
clang++ -std=c++23 vla.cpp -o vla
g++-16 -std=c++23 -pedantic-errors vla.cpp -o vla
""")

Output

$ g++-16 -std=c++23 -Wall -Wextra vla.cpp -o vla && echo 5 | ./vla
Last square: 16
$ g++-16 -std=c++23 -Wall -Wextra -Wpedantic vla.cpp -o vla
vla.cpp: In function 'int main()':
vla.cpp:6:9: warning: ISO C++ forbids variable length array 'squares' [-Wvla]
    6 |     int squares[n];
      |         ^~~~~~~
$ clang++ vla.cpp -o vla
$ clang++ -std=c++23 vla.cpp -o vla
vla.cpp:6:17: warning: variable length arrays in C++ are a Clang extension [-Wvla-cxx-extension]
    6 |     int squares[n];
      |                 ^
vla.cpp:6:17: note: read of non-const variable 'n' is not allowed in a constant expression
vla.cpp:4:9: note: declared here
    4 |     int n = 0;
      |         ^
1 warning generated.
$ g++-16 -std=c++23 -pedantic-errors vla.cpp -o vla
vla.cpp: In function 'int main()':
vla.cpp:6:9: error: ISO C++ forbids variable length array 'squares' [-Wvla]
    6 |     int squares[n];
      |         ^~~~~~~
[exit status 1]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 vla_session.py

GCC 16 compiled this without a word under -std=c++23 -Wall -Wextra. Only -Wpedantic made it say ISO C++ forbids variable length array 'squares' [-Wvla]. Clang 21 warned variable length arrays in C++ are a Clang extension [-Wvla-cxx-extension] as soon as -std=c++23 was given, and said nothing in its default GNU mode. With input 5 the program printed Last square: 16, so nothing looks wrong until the code meets a compiler without the extension, or a size large enough to overflow the stack. -pedantic-errors makes such code fail to compile, and std::vector<int> squares(n); is the standard way to write it.

-Werror: warnings that stop the build

A warning lets the build finish, and warnings that scroll past are easy to ignore. -Werror turns every warning into an error, so the program is not built until the code is fixed:

five_bugs.cpp with -Wall -Werror Python · werror_session.py
from session import run

run("""
clang++ -std=c++23 -Wall -Werror five_bugs.cpp -o five_bugs
g++-16 -std=c++23 -Wall -Werror five_bugs.cpp -o five_bugs
""")

Output

$ clang++ -std=c++23 -Wall -Werror five_bugs.cpp -o five_bugs
five_bugs.cpp:7:1: error: non-void function does not return a value in all control paths [-Werror,-Wreturn-type]
    7 | }
      | ^
five_bugs.cpp:11:9: error: unused variable 'count' [-Werror,-Wunused-variable]
   11 |     int count = 0;
      |         ^~~~~
2 errors generated.
[exit status 1]
$ g++-16 -std=c++23 -Wall -Werror five_bugs.cpp -o five_bugs
five_bugs.cpp: In function 'int main()':
five_bugs.cpp:13:23: error: comparison of integer expressions of different signedness: 'int' and 'std::vector<int>::size_type' {aka 'long unsigned int'} [-Werror=sign-compare]
   13 |     for (int i = 0; i < marks.size(); ++i) {
      |                     ~~^~~~~~~~~~~~~~
five_bugs.cpp:11:9: error: unused variable 'count' [-Werror=unused-variable]
   11 |     int count = 0;
      |         ^~~~~
five_bugs.cpp: In function 'int grade_points(int)':
five_bugs.cpp:7:1: error: control reaches end of non-void function [-Werror=return-type]
    7 | }
      | ^
cc1plus: all warnings being treated as errors
[exit status 1]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 werror_session.py

The message then names both flags: Clang wrote error: unused variable 'count' [-Werror,-Wunused-variable], GCC wrote error: unused variable 'count' [-Werror=unused-variable] and added cc1plus: all warnings being treated as errors.

-Werror=return-type makes only that warning an error, and -Wno-error=sign-compare keeps one warning a warning when -Werror is on. Both compilers document the same three forms. Turn -Werror on where a clean build is the rule, such as CI and work you hand in. While you are trying something out it can get in the way, and a newer compiler may add warnings that break a -Werror build on the day you upgrade.

Optimisation and debug flags

-O0, GCC’s documented default, compiles quickly and keeps the program close to the source, which is what a debugger needs. -O2 performs nearly all the optimisations that do not trade size for speed. -O3 adds more aggressive loop transformations. GCC also has -Og, which its manual calls the level of choice for the edit-compile-debug cycle. -g adds the debug information, and GCC’s manual notes that it can be combined with any -O level.

Never judge speed from a -O0 build. This program runs a hash over ten million numbers five times and reports how long that took:

The same work at different optimisation levels C++ · hash_timing.cpp
#include <chrono>
#include <cstdint>
#include <iostream>
#include <vector>

int main() {
    std::vector<int> values(10'000'000);
    for (std::size_t i = 0; i < values.size(); ++i) {
        values[i] = static_cast<int>(i % 1000);
    }

    const auto start = std::chrono::steady_clock::now();
    std::uint64_t hash = 0;
    for (int pass = 0; pass < 5; ++pass) {
        for (std::size_t i = 0; i < values.size(); ++i) {
            hash = hash * 31 + static_cast<std::uint64_t>(values[i]);
        }
    }
    const auto stop = std::chrono::steady_clock::now();

    const auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(stop - start).count();
    std::cout << "hash = " << hash << '\n';
    std::cout << "time: " << ms << " ms\n";
}
-O0 and -O2 with both compilers Python · timing_session.py
from session import run

# The same hash at -O0 and -O2 with each compiler. Times differ from run to run.
run("""
clang++ -std=c++23 -O0 hash_timing.cpp -o hash_timing && ./hash_timing
clang++ -std=c++23 -O2 hash_timing.cpp -o hash_timing && ./hash_timing
g++-16 -std=c++23 -O0 hash_timing.cpp -o hash_timing && ./hash_timing
g++-16 -std=c++23 -O2 hash_timing.cpp -o hash_timing && ./hash_timing
""")

Output

$ clang++ -std=c++23 -O0 hash_timing.cpp -o hash_timing && ./hash_timing
hash = 3334977592157182016
time: 221 ms
$ clang++ -std=c++23 -O2 hash_timing.cpp -o hash_timing && ./hash_timing
hash = 3334977592157182016
time: 48 ms
$ g++-16 -std=c++23 -O0 hash_timing.cpp -o hash_timing && ./hash_timing
hash = 3334977592157182016
time: 437 ms
$ g++-16 -std=c++23 -O2 hash_timing.cpp -o hash_timing && ./hash_timing
hash = 3334977592157182016
time: 55 ms

This output changes from run to run: the times differ from run to run and from one computer to another

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 timing_session.py

The recorded session is one run, so its times are only an example. Across repeated runs on this track’s build machine, an Apple M5 Pro:

Build Clang 21 GCC 16
-O0 140 to 290 ms 240 to 440 ms
-O2 40 to 50 ms 45 to 60 ms

Every build printed the same hash, 3334977592157182016; only the time changed. -O1 and -O3 landed within a few milliseconds of -O2 here, because each step of this loop waits for the one before it. Loops that the compiler can spread over vector instructions gain far more: a plain sum over the same ten million numbers ran about 30 times faster at -O2 than at -O0 with Clang. Your own numbers will differ with the machine and the load on it, so compare builds on the same computer, several runs each.

The same habits with MSVC

Microsoft’s compiler, cl.exe, spells its options differently:

GCC and Clang MSVC Note
-std=c++23 /std:c++latest /std:c++14 is MSVC’s default; Visual Studio 2022 17.13 and later also have /std:c++23preview
-Wall -Wextra /W4 /W1 is the command-line default and /W3 the IDE’s; Microsoft recommends /W4 for new projects
-Wpedantic /permissive- Conformance mode; /std:c++latest turns it on since Visual Studio 2019 16.8
-Werror /WX /we4996 makes one warning an error, /wd4996 silences one
-O0 / -O2 /Od / /O2
-g /Zi Debug information in a separate PDB file
(nothing to add) /EHsc Without an /EH option, an exception does not destroy the local objects it passes outside try blocks; Microsoft recommends /EHsc

MSVC’s /Wall is not the same as GCC’s -Wall: it shows everything /W4 shows plus the warnings that are off by default, which is far noisier. Clang’s -Weverything is similar, and Clang’s manual says that -Wall -Wextra is the better choice for most projects. A typical command is cl /std:c++latest /W4 /permissive- /EHsc /Zi main.cpp.

Exercise: build five_bugs.cpp without a single warning

Change five_bugs.cpp so that both commands below finish with no output at all, and the program prints the three grade lines and the true average:

  • clang++ -std=c++23 -Wall -Wextra -Wpedantic -Wconversion -Wshadow -Werror five_bugs.cpp
  • g++-16 -std=c++23 -Wall -Wextra -Wpedantic -Wconversion -Wshadow -Werror five_bugs.cpp (or g++ with your GCC)

Hint: fix one warning at a time, starting from the first. Removing a warning without fixing its cause, for example with a cast that hides a real loss, does not count.

One solution

five_bugs.cpp, fixed C++ · five_bugs_fixed.cpp
#include <iostream>
#include <vector>

int grade_points(int mark) {
    if (mark >= 75) return 10;
    if (mark >= 50) return 7;
    return 0;
}

int main() {
    const std::vector<int> marks{82, 57, 69};
    double total = 0;
    for (int mark : marks) {
        total += mark;
    }
    const double average = total / static_cast<double>(marks.size());
    for (int mark : marks) {
        const int points = grade_points(mark);
        std::cout << mark << " -> " << points << " points\n";
    }
    std::cout << "Average mark: " << average << '\n';
}
The fixed file with every flag and -Werror Python · fixed_session.py
from session import run

run("""
clang++ -std=c++23 -Wall -Wextra -Wpedantic -Wconversion -Wshadow -Werror five_bugs_fixed.cpp -o five_bugs_fixed && ./five_bugs_fixed
g++-16 -std=c++23 -Wall -Wextra -Wpedantic -Wconversion -Wshadow -Werror five_bugs_fixed.cpp -o five_bugs_fixed && ./five_bugs_fixed
""")

Output

$ clang++ -std=c++23 -Wall -Wextra -Wpedantic -Wconversion -Wshadow -Werror five_bugs_fixed.cpp -o five_bugs_fixed && ./five_bugs_fixed
82 -> 10 points
57 -> 7 points
69 -> 7 points
Average mark: 69.3333
$ g++-16 -std=c++23 -Wall -Wextra -Wpedantic -Wconversion -Wshadow -Werror five_bugs_fixed.cpp -o five_bugs_fixed && ./five_bugs_fixed
82 -> 10 points
57 -> 7 points
69 -> 7 points
Average mark: 69.3333

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 fixed_session.py

grade_points now returns 0 for marks under 50, the unused variable is gone, a range-based for removes the index and the signed comparison with it, the average is a double, and the inner variable has its own name, points. Both compilers built it with no output under those flags, and it printed the three grade lines and Average mark: 69.3333.

Check yourself

Check yourself

5 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.

  1. Question 1 of 5 A loop declares a variable with the same name as one outside it. Which flag made both Clang 21 and GCC 16 report it in the lesson's program?

    Choose one answer.

    Show the answer to question 1

    Answer: -Wshadow

    Neither -Wall nor -Wextra includes -Wshadow on either compiler, so the inner average was reported only once -Wshadow was added. That is why the lesson's full set names -Wconversion and -Wshadow separately.

  2. Question 2 of 5 In the lesson's run, which compiler reported the comparison of an int with marks.size() under -Wall alone?

    Choose one answer.

    Show the answer to question 2

    Answer: GCC 16; Clang 21 needed -Wextra

    GCC's manual puts -Wsign-compare in -Wall for C++, while Clang's reference puts it in -Wextra. The two compilers group warnings differently, which is one reason to build with both and to use -Wall and -Wextra together.

  3. Question 3 of 5 A program prints one result when built with -O0 and another with -O2. What is the most likely cause?

    Choose one answer.

    Show the answer to question 3

    Answer: The program has undefined behaviour, such as a value-returning function that can reach its end without return

    Optimisation must not change what a correct program does, but a program with undefined behaviour has no correct result to keep. The lesson's missing return printed 1 point at -O0 and 7 points at -O2 with Clang. Warnings such as -Wreturn-type and run-time checks such as -fsanitize=undefined find these mistakes.

  4. Question 4 of 5 Which MSVC option does the job of -Werror?

    Choose one answer.

    Show the answer to question 4

    Answer: /WX

    /WX treats all compiler warnings as errors. /W4 sets the warning level, /Wall shows every warning including those that are off by default, and /permissive- is the standards conformance mode.

  5. Question 5 of 5 Which statements about timing a program are true? Choose every true one.

    Choose every answer that is right.

    Show the answer to question 5

    Answer:

    • Compare builds on the same computer, with several runs each
    • Time optimised builds such as -O2, never -O0 builds

    In the lesson the -O0 builds took about 3 to 10 times as long as the -O2 builds for the same work, so -O0 timings say little. Times vary between runs and machines. -g adds debug information and can be combined with any -O level, and warning flags change only what the compiler reports, not the code it produces.

Interview questions

What is the difference between -Wall and -Wextra, and does -Wall turn on all warnings? (warm-up)

No. -Wall turns on a set of common warnings that are easy to avoid, such as unused variables and suspicious parentheses, and -Wextra adds more, such as unused parameters and, on Clang, signed and unsigned comparisons. Many useful warnings are in neither set: -Wshadow, -Wconversion and the standard-conformance warnings of -Wpedantic must be asked for by name. The two compilers also group warnings differently: GCC puts -Wsign-compare in -Wall for C++, while Clang puts it in -Wextra. A good default is -Wall -Wextra -Wpedantic, with -Wconversion and -Wshadow added when the extra reports are worth reading, and the bracketed flag name on each warning shows where it came from.

Key takeaways

  • Build with -std=c++23 -Wall -Wextra -Wpedantic; without -std, Apple clang 21 compiles C++14 and GCC 16 C++20.
  • -Wall is not every warning: -Wconversion and -Wshadow found mistakes that -Wall -Wextra missed, and Clang and GCC put some warnings in different groups.
  • A missing return is undefined behaviour: the same file printed 1, printed 7 or trapped, depending on the compiler and the -O level. Make -Werror=return-type a habit and use -fsanitize=address,undefined to catch it at run time.
  • -Wpedantic reports extensions such as variable-length arrays that other compilers may reject.
  • Debug with -O0 -g (or GCC’s -Og -g), time only optimised builds, and keep the warning flags in every build.
  • MSVC’s equivalents are /std:c++latest /W4 /permissive- /EHsc, with /WX for errors, /Od or /O2, and /Zi.

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.