If you are a C++ programmer, you know that declaring a global variable without initialising it is safe because it is placed in the BSS section and the compiler makes sure it is zero-filled. However, the same is not true for stack variables, or local or automatic variables. If you haven’t initialised your local variables, they are not automatically initialised by the compiler, as there is no BSS section in the stack.
C++23 local variable
Let’s have a look at the following sample code:
#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()
{
open_device("modem", "port=/dev/ttyS1 timeout_ms=2500");
open_device("logger", "port=/dev/ttyS2 retries=3");
}Now, let’s compile the program with optimisation level set to O0:
g++ -std=c++23 -O0 -g config_timeout.cpp -o timeout_cxx23At optimisation level O0, we can verify the uninitialised state of the local variable timeout_ms declared in the function read_timeout_ms(). Let’s run it with Valgrind now:
valgrind --track-origins=yes ./timeout_cxx23
==457304== Memcheck, a memory error detector
==457304== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==457304== Using Valgrind-3.18.1 and LibVEX; rerun with -h for copyright info
==457304== Command: ./timeout_cxx23
==457304==
modem timeout 2500 ms
==457304== Conditional jump or move depends on uninitialised value(s)
==457304== at 0x401268: open_device(char const*,
std::basic_string_view<char, std::char_traits<char> >)
(config_timeout.cpp:28)
==457304== by 0x4012FC: main (config_timeout.cpp:41)
==457304== Uninitialised value was created by a stack allocation
==457304== at 0x401166: read_timeout_ms(
std::basic_string_view<char,
std::char_traits<char> >)
(config_timeout.cpp:10)
==457304==
logger no timeout configured, using default 1000 ms
==457304==
==457304== HEAP SUMMARY:
==457304== in use at exit: 0 bytes in 0 blocks
==457304== total heap usage: 2 allocs, 2 frees, 74,752 bytes allocated
==457304==
==457304== All heap blocks were freed -- no leaks are possible
==457304==
==457304== For lists of detected and suppressed errors, rerun with: -s
==457304== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)
Zoom in on the following lines:
==457304== Uninitialised value was created by a stack allocation
==457304== at 0x401166: read_timeout_ms(std::basic_string_view<char,
std::char_traits<char> >)
(config_timeout.cpp:10)
==457304== It is complaining about the variable timeout_ms in the function read_timeout_ms(). Now, let’s take a look at what happens if we simply compile the above program at optimisation level O0 with a C++26 compiler and run Valgrind on the executable.
C++26 local variable
Compile the above code with C++26 as follows:
g++ -std=c++26 -O0 -g config_timeout.cpp -o timeout_cxx26I have explained the C++26 compiler setup in one of the previous posts GCC 16 setup guide.
Then run the executable timeout_cxx26 with Valgrind:
valgrind --track-origins=yes ./timeout_cxx26
==426828== Memcheck, a memory error detector
==426828== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==426828== Using Valgrind-3.18.1 and LibVEX; rerun with -h for copyright info
==426828== Command: ./timeout_cxx26
==426828==
modem timeout 2500 ms
logger no timeout configured, using default 1000 ms
==426828==
==426828== HEAP SUMMARY:
==426828== in use at exit: 0 bytes in 0 blocks
==426828== total heap usage: 2 allocs, 2 frees, 74,752 bytes allocated
==426828==
==426828== All heap blocks were freed -- no leaks are possible
==426828==
==426828== For lists of detected and suppressed errors, rerun with: -s
==426828== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)Notice that Valgrind is no longer complaining about the uninitialised variable timeout_ms in the function read_timeout_ms(). The reason is that GCC 16 zero-fills local variables when compiling in C++26 mode.
Optimisation level O2
The behaviour is slightly different if we set the optimisation level to O2. At optimisation level O2, Valgrind is not quite suitable to see the difference because the compiler starts using registers to move around the variables and their initialisation. But we can see the difference from the assembly. Generate the assembly code out of the source code for each compiler as follows:
C++26
g++ -std=c++26 -O2 -S -fverbose-asm config_timeout.cpp -o v_cxx26.SThis generates the assembly code with the help of the C++26 compiler, optimisation set at O2 level, and the -fverbose-asm switch adds comments showing which source line each instruction comes from. Now, if you load the assembly code in an editor, you should be able to locate the assembly code below:
_Z15read_timeout_msSt17basic_string_viewIcSt11char_traitsIcEE:
.LFB2500:
.cfi_startproc
subq $40, %rsp #,
.cfi_def_cfa_offset 48
movq %rbx, (%rsp) #,
.cfi_offset 3, -48
# config_timeout.cpp:11: int timeout_ms;
xorl %ebx, %ebx # <retval>This is the part of the assembly generated for the read_timeout_ms() function, and the line below is making sure that the variable timeout_ms is initialised to zero:
xorl %ebx, %ebx # <retval>It is an XOR operation, and simply put, as both the operands are ebx, the output of the operation will be zero.
C++23
You can generate the same assembly code using C++23 as well, but the same zero initialisation will not be found in the case of C++23. Do the following:
g++ -std=c++23 -O2 -S -fverbose-asm config_timeout.cpp -o v_cxx23.SIf you examine the assembly code generated for the function read_timeout_ms(), you won’t be able to see the same initialisation code for the timeout_ms variable.
So there you go, it is now proven, and hence all the more reason for you to try it yourself.
The sample source code and the generated assembly code can be found in the below provided gitrepository.
Source code cpp_26_uninitialised The complete example from this post, ready to build with GCC 16. View on GitHubDiscover more from Tech For Talk
Subscribe to get the latest posts sent to your email.
Leave a Reply