Read compiler warnings
A warning is a bug the compiler has already found. It compiles the program anyway, because the code is legal, but it tells you that the code is suspicious: a name that is never used, a comparison between signed and unsigned values, a variable read before it was given a value. Reading those messages is the cheapest debugging there is.
This lesson turns the useful warning groups on, shows five programs that produce real GCC messages, and ends with the flag that turns every warning into an error so a build cannot ignore one.
Three flags that pay for themselves
-Wall turns on the warnings that almost always indicate a bug, -Wextra adds a set that is noisier but useful, and -Wpedantic reports anything that is not standard C++. Together they cost nothing at run time: the binary is the same, only the diagnostics change.
A clean program stays clean with them on, which is the point. The program below is written the way the course writes code, and it compiles without a single warning under all three flags.
Make these flags part of every command. When a project has a build file, the flags belong there, so nobody has to remember them; lesson 006 adds a header and a second source file, and the flags travel with the compile commands.
#include <iostream>
#include <vector>
int main()
{
std::vector<int> values{4, 8, 15, 16, 23, 42};
int total = 0;
for (int value : values) {
total += value;
}
std::cout << "total: " << total << '\n';
}
g++ -std=c++23 -Wall -Wextra -Wpedantic clean.cpp -o clean
./clean
total: 108
Unused variables and parameters
An unused local variable is dead code, and an unused parameter usually means the function was copied and half edited. -Wall reports both, with the name and the flag, and the program still compiles.
The function below takes a height, ignores it and returns width * width. That is the bug the warning points at: the parameter is unused because the body forgot it, and the area of 3 by 4 comes out as 9.
When a parameter is unused on purpose, leave the name out of the definition or mark it with [[maybe_unused]]. Silencing a warning is a decision, and the attribute records it in the source.
#include <iostream>
int area(int width, int height)
{
int unused = 42;
return width * width;
}
int main()
{
std::cout << area(3, 4) << '\n';
}
g++ -std=c++23 -Wall -Wextra unused.cpp -o unused
./unused
unused.cpp: In function 'int area(int, int)':
unused.cpp:5:9: warning: unused variable 'unused' [-Wunused-variable]
5 | int unused = 42;
| ^~~~~~
unused.cpp:3:25: warning: unused parameter 'height' [-Wunused-parameter]
3 | int area(int width, int height)
| ~~~~^~~~~~
9
Signed and unsigned comparisons
std::vector::size returns std::size_t, an unsigned type, and an int loop counter is signed. The comparison between them converts the int to unsigned, which is correct for every non-negative value and wrong for a negative one, so the compiler warns.
The code below is safe in practice because i starts at 0, and it is still the wrong habit: a loop that counts backwards with an int would turn -1 into a huge unsigned number and read out of bounds. -Wsign-compare is the warning that catches the pattern before it becomes a loop that runs forever.
The fix is to make the counter the same type as the size, or to use a range-for loop when the index is not needed. Both keep the comparison in one signedness.
#include <iostream>
#include <vector>
int main()
{
std::vector<int> values{1, 2, 3};
int total = 0;
for (int i = 0; i < values.size(); ++i) {
total += values[i];
}
std::cout << total << '\n';
}
g++ -std=c++23 -Wall -Wextra sign_compare.cpp -o sign_compare
./sign_compare
sign_compare.cpp: In function 'int main()':
sign_compare.cpp:8:23: warning: comparison of integer expressions of different signedness: 'int' and 'std::vector<int>::size_type' {aka 'long unsigned int'} [-Wsign-compare]
8 | for (int i = 0; i < values.size(); ++i) {
| ~~^~~~~~~~~~~~~~~
6
Values that were never set
A local int has no default value. Reading it before assignment is undefined behaviour, and the warning that reports it needs optimisation to see the data flow, so it appears with -O2 and sometimes not at all.
The program below uses limit in the loop condition without ever setting it. The compiler reports the read and names the variable; the program is not run here, because whatever the loop does with an indeterminate value is not something a lesson should print.
Zero-initialise a local when it is declared, or declare it where the value comes from. The compiler cannot warn about every uninitialised read, so the habit matters more than the flag.
#include <iostream>
int main()
{
int limit;
int total = 0;
for (int i = 0; i < limit; ++i) {
total += i;
}
std::cout << total << '\n';
}
g++ -std=c++23 -O2 -Wall -Wextra uninitialized.cpp -o uninitialized
uninitialized.cpp: In function 'int main()':
uninitialized.cpp:7:23: warning: 'limit' is used uninitialized [-Wuninitialized]
7 | for (int i = 0; i < limit; ++i) {
| ~~^~~~~~~
uninitialized.cpp:5:9: note: 'limit' was declared here
5 | int limit;
| ^~~~~
-Werror and -Wconversion
-Werror turns every warning into an error, so a build that used to print a warning now fails. That is what a project with several developers wants: a warning nobody reads is a warning that stays forever.
-Wconversion adds warnings for implicit conversions that lose information. The program below multiplies a double by 4 and stores the result in an int; the value happens to be exact, and the warning is still right, because the next value may not be.
With -Werror the compiler stops, prints the error and leaves no binary behind. The fix is an explicit cast, static_cast<int>(ratio * 4), which says in the source that the truncation is intended.
#include <iostream>
int main()
{
double ratio = 0.75;
int count = ratio * 4;
std::cout << count << '\n';
}
g++ -std=c++23 -Wall -Wextra -Wconversion -Werror werror.cpp -o werror
werror.cpp: In function 'int main()':
werror.cpp:6:23: error: conversion from 'double' to 'int' may change value [-Werror=float-conversion]
6 | int count = ratio * 4;
| ~~~~~~^~~
cc1plus: all warnings being treated as errors
Exercises
- Compile unused.cpp with -Wall -Wextra -Werror and read the first error. Fix the function, then add [[maybe_unused]] to a deliberately unused parameter and check that the build passes.
- Rewrite the loop in sign_compare.cpp with a std::size_t counter and with a range-for. Which version warns, and which is easier to read?
- Add -Wshadow to the flags and write a program that hides an outer variable in an inner block. What does the warning name?
Questions
A new set of questions in a new order every time. Choose one answer for each, then check.
-
What is a compiler warning?
Answer
A legal program the compiler finds suspicious
The code compiles, and the warning points at something that is probably a bug.
-
What does
-Werrordo?Answer
It turns every warning into an error and stops the build
The compiler treats the diagnostic as fatal, which is how a project keeps a warning-free tree.
-
What does
-Wconversionwarn about?Answer
Implicit conversions that may change the value
The group is about narrowing or sign-changing conversions that happen without a cast.
-
This code is built with
g++ -std=c++23 -O2 -Wall -Wextra q.cpp. Why does the warning aboutlimitneed the optimiser?int limit; int total = 0; for (int i = 0; i < limit; ++i) { total += i; } std::cout << total << '\n';Answer
The optimiser makes the use of the uninitialised value visible
At -O2 the compiler can see the read before the write, and the warning becomes reliable.
-
What does this program print?
#include <iostream> int main() { int total = 0; for (int i = 1; i <= 8; ++i) total += i * 3; std::cout << "total: " << total; }Answer
total: 108
The loop adds 3, 6, ..., 24, and the sum is 108.
-
The compiler warns about an unused parameter. What does the program print?
#include <iostream> int area(int width, int height) { return width * width; } int main() { std::cout << area(3, 9); }Answer
9
A warning does not stop the build, and the function returns 3 times 3 while the second argument is unused.
-
The compiler warns about a signed comparison. What does the program print?
#include <iostream> #include <vector> int main() { std::vector<int> values(6, 1); int total = 0; for (int i = 0; i < values.size(); ++i) total += values[i]; std::cout << total; }Answer
6
The loop adds the six elements, and the warning is about the comparison, not the sum.
-
Which warning does
for (int i = 0; i < values.size(); ++i)produce?Answer
-Wsign-compare
An int is compared with a size_type, and the compiler names the mismatch.
-
The build command is
g++ -std=c++23 -Wconversion -Werror q.cpp. What does it report for these lines?double ratio = 1.5; int count = ratio * 4;Answer
conversion from 'double' to 'int' may change value, then all warnings are errors
The double-to-int initialisation is a conversion, and -Werror makes that warning fatal.
-
The build command is
g++ -std=c++23 -O2 -Wall -Wextra q.cpp. What does the compiler warn about?int limit; int total = 0; for (int i = 0; i < limit; ++i) total += i;Answer
'limit' is used uninitialized
At -O2 the read of limit before any write is visible, and the message names the variable.
The code on this page was run with g++ 15.2.0 on Alpine Linux 3.24.3 (x86_64) in a Docker container.
Article author: Arthur Isaev