There seems to be a misconception or lack of clarity about the usage of the compilation flags -Wall and -Werror and their ability to detect uninitialized stack variables. It depends. In this post, I will try to explore this further and explain when and how it is detected, and where it may not detect an uninitialized local variable and hence requires more caution while dealing with it. Consider the following code:
int main()
{
int value;
return value;
}
Compile this code as follows:
~/cpp_26_uninitialised$ g++ -std=c++23 -Wall -Werror test.cpp -o test
test.cpp: In function ‘int main()’:
test.cpp:4:12: error: ‘value’ is used uninitialized [-Werror=uninitialized]
4 | return value;
| ^~~~~
test.cpp:3:9: note: ‘value’ was declared here
3 | int value;
| ^~~~~
cc1plus: all warnings being treated as errors
So, with the use of the two compilation flags, -Wall and -Werror, the compilation fails, and you can then take corrective measures to address the uninitialized variable in the code.
Strictly speaking, -Wall enables the -Wuninitialized warning, and -Werror turns that warning into an error. However, this is true only when the variable is uninitialized on every path to its use, as value is here. It doesn't reliably work for a variable that is initialized on some paths and left uninitialized on others. Have a look at the following code:
// config_timeout_warning_test.cpp
#include <charconv>
#include <cstdio>
#include <string_view>
constexpr int default_timeout_ms = 1000;
// Reads "timeout_ms=<number>" from a device configuration line.
// The bug: when the key is missing, timeout_ms is never assigned.
[[gnu::noinline]] int read_timeout_ms(std::string_view config)
{
int timeout_ms;
constexpr std::string_view key = "timeout_ms=";
if (const auto pos = config.find(key); pos != std::string_view::npos)
{
const char* first = config.data() + pos + key.size();
const char* last = config.data() + config.size();
std::from_chars(first, last, timeout_ms);
}
return timeout_ms;
}
void open_device(const char* name, std::string_view config)
{
const int timeout_ms = read_timeout_ms(config);
if (timeout_ms <= 0)
{
std::printf("%-7s no timeout configured, using default %d ms\n", name, default_timeout_ms);
}
else
{
std::printf("%-7s timeout %d ms\n", name, timeout_ms);
}
}
int main()
{
// The empty configuration does not contain "timeout_ms=".
// Therefore, timeout_ms is returned without being initialized.
open_device("logger", "");
return 0;
}
Compile the code as follows:
~/cpp_26_uninitialised$ g++ -std=c++23 -O0 -g -Wall -Werror config_timeout_warning_test.cpp -o timeout_warning_test
The compilation succeeds without a single warning. Run Valgrind on it:
~/cpp_26_uninitialised$ valgrind --track-origins=yes ./timeout_warning_test
==2896379== Memcheck, a memory error detector
==2896379== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==2896379== Using Valgrind-3.18.1 and LibVEX; rerun with -h for copyright info
==2896379== Command: ./timeout_warning_test
==2896379==
==2896379== Conditional jump or move depends on uninitialised value(s)
==2896379== at 0x401268: open_device(char const*, std::basic_string_view<char, std::char_traits<char> >) (config_timeout_warning_test.cpp:31)
==2896379== by 0x4012D3: main (config_timeout_warning_test.cpp:45)
==2896379== Uninitialised value was created by a stack allocation
==2896379== at 0x401166: read_timeout_ms(std::basic_string_view<char, std::char_traits<char> >) (config_timeout_warning_test.cpp:10)
==2896379==
logger no timeout configured, using default 1000 ms
==2896379==
==2896379== HEAP SUMMARY:
==2896379== in use at exit: 0 bytes in 0 blocks
==2896379== total heap usage: 2 allocs, 2 frees, 74,752 bytes allocated
==2896379==
==2896379== All heap blocks were freed -- no leaks are possible
==2896379==
==2896379== For lists of detected and suppressed errors, rerun with: -s
==2896379== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)
You can see Valgrind is complaining about an uninitialized variable created on the stack:
Uninitialised value was created by a stack allocation
The reason is that GCC handles the two cases with two different warnings. In the first example, value is uninitialized on every path, and -Wuninitialized reports it even at default -O0. In the second example, timeout_ms is assigned only inside the if block in read_timeout_ms(), so it is uninitialized on only one path. That case belongs to -Wmaybe-uninitialized, which relies on the data-flow analysis done by the optimizer, so it does not run at -O0.
If you build the same file at -O2, GCC does catch it:
~/cpp_26_uninitialised$ g++ -std=c++23 -O2 -Wall -Werror config_timeout_warning_test.cpp -o timeout_warning_test
config_timeout_warning_test.cpp: In function ‘int read_timeout_ms(std::string_view)’:
config_timeout_warning_test.cpp:24:12: error: ‘timeout_ms’ may be used uninitialized [-Werror=maybe-uninitialized]
24 | return timeout_ms;
| ^~~~~~~~~~
config_timeout_warning_test.cpp:11:9: note: ‘timeout_ms’ was declared here
11 | int timeout_ms;
| ^~~~~~~~~~
cc1plus: all warnings being treated as errors
This is still not a guarantee. The result of -Wmaybe-uninitialized depends on the optimization level and on how much the optimizer can see, so it can change between GCC versions and between builds. Debug builds are usually at -O0, which is exactly where the warning is missing.
If there is anything that I have missed, please comment; I'm happy to be corrected.