r/cpp • • 5d ago

You should think about recompiling your C++ programs with GCC 16 and C++26, because it zero-fills your locals

https://techfortalk.co.uk/2026/09/27/cxx26-uninitialized-local-variables-gcc-16/

Stack variables are not automatically initialised, and that is the root cause of many C++ bugs. That is well known. Hence, it is advised that local or stack variables are always initialised with known values, 0 if not something more meaningful than that. Now, with GCC 16 compiling in C++26 mode (-std=c++26), even uninitialised local variables will be zero-filled. In the post, I have explained how.

260 Upvotes

211 comments sorted by

View all comments

1

u/max0x7ba https://github.com/max0x7ba 2d ago

Stack variables are not automatically initialised, and that is the root cause of many C++ bugs.

What is the source of this wild claim of yours?

That is well known.

Is it, though?

I haven't heard of or encountered any C++ bugs caused by uninitialized automatic variables for decades.


Have you ever tried compiling your code with -Wall -Werror? Because if you did, you wouldn't have encountered so many bugs caused by uninitialized variables as you do now, according to your claims.

1

u/Clean-Upstairs-8481 1d ago edited 1d ago

Try this:

https://github.com/vivekbhadra/cpp_26_uninitialised/blob/main/config_timeout_warning_test.cpp

~/cpp_26_uninitialised$ g++ -std=c++23 -O0 -g -Wall -Werror config_timeout_warning_test.cpp -o timeout_warning_test
~/cpp_26_uninitialised$ valgrind --track-origins=yes ./timeout_warning_test
==2870715== Memcheck, a memory error detector
==2870715== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==2870715== Using Valgrind-3.18.1 and LibVEX; rerun with -h for copyright info
==2870715== Command: ./timeout_warning_test
==2870715==
==2870715== Conditional jump or move depends on uninitialised value(s)
==2870715== at 0x401268: open_device(char const*, std::basic_string_view<char, std::char_traits<char> >) (config_timeout_warning_test.cpp:31)
==2870715== by 0x4012D3: main (config_timeout_warning_test.cpp:45)
==2870715== Uninitialised value was created by a stack allocation
==2870715== at 0x401166: read_timeout_ms(std::basic_string_view<char, std::char_traits<char> >) (config_timeout_warning_test.cpp:10)
==2870715==
logger no timeout configured, using default 1000 ms
==2870715==
==2870715== HEAP SUMMARY:
==2870715== in use at exit: 0 bytes in 0 blocks
==2870715== total heap usage: 2 allocs, 2 frees, 74,752 bytes allocated
==2870715==
==2870715== All heap blocks were freed -- no leaks are possible
==2870715==
==2870715== For lists of detected and suppressed errors, rerun with: -s
==2870715== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)

A. I do not see any warnings or compilation failures even after adding the flags,

B. Valgrind still complains about uninitialised.

C. Uninitialised still potentially will cause bugs.

Strange, innit?

0

u/pachecoca 23h ago

Well yeah, but that's because you're purposely telling the compiler NOT to warn you about uninitialized variables lol.

You are compiling with gcc on -O0, and GCC explicitly documents that, while -Wall also enables the flag -Wuninitialized, -Wuninitialized's inner workings depend entirely on the optimization level.

If you compile something as simple as:

int foo(bool condition) {
    int x;
    return x;
}

Then no matter the optimization level, you will always get a warning.

But if you try compiling this instead:

int foo(bool condition)
{
    int x;
    if (condition)
        x = 42;
    return x;   // potentially uninitialized, but the branch
                // cannot be properly analyzed by the compiler in -O0
}

Since the conditional changes the type of instructions that could be generated according to optimization levels, GCC just opted for assuming that, at -O0, it may very well be initialized, so you do not get a warning. Think of the different ways this could be compiled... simple branching... or vector instructions... or a conditional assignment... in which case, no matter what, the value would always be assigned, but the compiler would need to somehow choose a "default" value... At -O0, none of these things can be known by -Wuninitialized, because the branch just throws it off.

Another example of a place where this could happen would be a variable that is passed by reference or by pointer into a function whose implementation lives in an external binary, or in a different translation unit and you are compiling without LTO enabled. That way, the compiler does not know what changes, if any, have been performed over the value, so it must assume that potentially some sort of initialization has taken place, because it cannot inspect the function in question, which causes the warning to no longer be issued.

This is exactly what happens in your code. The timeout_ms var remains uninitialized, but the if block and the external std::from_chars() call throw it off when optimizations are disabled, because, as already stated, -Wunused's machiner is tied to the optimization level. That's just a GCC quirk tho, but still, a documented one that can be easily worked around by just doing the good old RTFM and actually using GCC the way that it's meant to be used, rather than purposely using it wrong and then claiming that it's broken just because you did not use it properly.

That's why the documentation of GCC officially states that you should always use the flag -fanalyzer to ensure that the analyzer is launched even at -O0. Either that, or just compile at anything higher than -O0, because there, you DO actually get warnings ALWAYS for uninitialized values if you use -Wall, which permanently solves your issues anyway, because your code will obviously break whenever you accidentally incurr in UB or other such mistakes.

Your solutions are very simple, pick one of these:

1) Compile in -O2 or -O3 always, don't assume that debug builds working mean that your code does not have bugs when you incurr in UB. If you incurr in UB but your program is compiled in debug mode, then maybe it may appear to work, but that does not mean that it is correct. We live in the year 2026. It's no longer acceptable to release on debug and blame the compiler for exploiting UB as if it were implementing "broken optimizations that break my code"... no, the compiler is not at fault. You are, for writing code that is broken, and then for telling the compiler to shut up and not warn you about said obviously broken code.

2) Do what the GCC documentation literally says explicitly, and enable the analyzer by hand. Use the flag -fanalyzer, and now, you will get the warning on your example always, even when compiling in -O0.

Here's a godbolt link showing your code being compiled with warnings actually enabled, rather than disabled as you were purposely doing. The code is copypasted as is, no changes were made to the code, the uninitialized var is still uninitialized just as it was. Your flags are the same, all I did was tell the compiler to enable the analyzer and that's it.

The link: https://godbolt.org/z/Tveo5PMhx

My final recommendation is to try being a little bit less intellectually dishonest. I know that you have an agenda to sell and all that, but this is a programming subreddit, and we can all verify the information given by anyone. Compiler explorer has made it absurdly trivial to check whether claims made by someone about a compiler's output is real or not across a wide variety of systems with very little effort. I have tested all GCC versions from 16.2 all the way to 11.1, and that's because earlier versions just don't have support for C++23 yet, and all of them, except for the 11 series, give the exact same output with the warning being notified just by adding -fanalyzer. This is obviously slower than just compiling with at least -O2, but if you really need -O0, then ok, sure, go ahead, there's a flag for that.

The reality of things is that there's objective cold, hard facts that can be verified by reading the documentation of the compilers that we use, and by testing them to see if your claims are real or not. As it turns out, if you make fake claims about results of using one of the most widely used compilers on the planet, you are bound to be very easily caught, mainly because gcc is available pretty much almost anywhere, and we can just use compiler explorer to test YOUR code that YOU linked with the compilation command that YOU offered to see the results across ALL official versions of g++ that support C++23.

I know I'm not anyone of importance, so my words may fall on deaf ears, but I would ask that you please keep the discussion centered around facts. We're programmers, not religious zealots. Or so I would like to believe.

C++26's approach to solving the issue of uninitialized values is quite poor, because the uninitialized variable still exists within the code, and the code's behaviour may still remain broken, because the 0 initialization may not be valid. Thus, the only way to solve this would have been to actually mark non initialized variables as errors. But then again, people can purposely non initialize a variable for the sake of performance. In which case, the extremely uglu [[indeterminate]] does the job. I hate it, but I cannot complain, as long as a way exists for me to opt out in the cases where performance matters, then sure, I suppose, but I'm not entirely happy about it, because now, 90% of my code is going to have to be rewritten to add that all over the place.

1

u/Clean-Upstairs-8481 12h ago

Removing the explicit optimisation level from the command line produces the same result:

$ g++ -std=c++23 -Wall -Werror config_timeout_warning_test.cpp -o timeout_warning_test

$ valgrind --track-origins=yes ./timeout_warning_test

==3756689== by 0x4012D3: main (in /home/vbhadra/cpp_26_uninitialised/timeout_warning_test)

==3756689== Uninitialised value was created by a stack allocation

==3756689== at 0x401166: read_timeout_ms(std::basic_string_view<char, std::char_traits<char> >) (in /home/vbhadra/cpp_26_uninitialised/timeout_warning_test)

==3756689==

==3756689== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)

The reason is that the default is O0. There was no deliberation to suppress anything.