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.
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
What you’ll learn · Before you start
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
On this page
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.
#include <iostream>
int main() {
std::cout << __cplusplus << '\n';
} 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.
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.
- Writing and learning: add -g, so a debugger can show names and line numbers.
- Hunting a bug: add -O0 -g and -fsanitize=address,undefined, which check memory use and undefined behaviour while the program runs.
- Timing, contests and release: add -O2, the optimised build to time and ship.
- 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:
#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:
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:
#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';
} 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.
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:
#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";
}
} 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:
#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';
} 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:
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:
#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";
} 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.cppg++-16 -std=c++23 -Wall -Wextra -Wpedantic -Wconversion -Wshadow -Werror five_bugs.cpp(org++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
#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';
} 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.
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. -Wallis not every warning:-Wconversionand-Wshadowfound mistakes that-Wall -Wextramissed, and Clang and GCC put some warnings in different groups.- A missing
returnis undefined behaviour: the same file printed 1, printed 7 or trapped, depending on the compiler and the-Olevel. Make-Werror=return-typea habit and use-fsanitize=address,undefinedto catch it at run time. -Wpedanticreports 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/WXfor errors,/Odor/O2, and/Zi.
References
- Options to Request or Suppress Warnings (GCC manual) (GNU Project)
- Options That Control Optimization (GCC manual) (GNU Project)
- Options for Debugging Your Program (GCC manual) (GNU Project)
- Program Instrumentation Options (GCC manual) (GNU Project)
- GCC 16 Release Series, Changes, New Features, and Fixes (GNU Project)
- Clang Compiler User's Manual (LLVM Project)
- Diagnostic flags in Clang (LLVM Project)
- /w, /W0, /W1, /W2, /W3, /W4, /Wall, /wd, /we, /wo, /Wv, /WX (Warning level) (Microsoft)
- /permissive- (Standards conformance) (Microsoft)
- /std (Specify Language Standard Version) (Microsoft)
- /Z7, /Zi, /ZI (Debug Information Format) (Microsoft)
- /O options (Optimize code) (Microsoft)
- /EH (Exception handling model) (Microsoft)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress