Skip to content(if available)orjump to list(if available)

GCC 15.1

GCC 15.1

157 comments

·April 25, 2025

Calavar

> {0} initializer in C or C++ for unions no longer guarantees clearing of the whole union (except for static storage duration initialization), it just initializes the first union member to zero. If initialization of the whole union including padding bits is desirable, use {} (valid in C23 or C++) or use -fzero-init-padding-bits=unions option to restore old GCC behavior.

This is going to silently break so much existing code, especially union based type punning in C code. {0} used to guarantee full zeroing and {} did not, and step by step we've flipped the situation to the reverse. The only sensible thing, in terms of not breaking old code, would be to have both {0} and {} zero initialize the whole union.

I'm sure this change was discussed in depth on the mailing list, but it's absolutely mind boggling to me

nikic

Fun fact: GCC decided to adopt Clang's (old) behavior at the same time Clang decided to adopt GCC's (old) behavior.

So now you have this matrix of behaviors: * Old GCC: Initializes whole union. * New GCC: Initializes first member only. * Old Clang: Initializes first member only. * New Clang: Initializes whole union.

augusto-moura

That's funny and sad at the same time.

And it shows a deeper problem, even though they are willing to align behavior between each other, they failed to communicate and discuss what would be the best approach. That's a bit tragic, IMO

Neywiny

I would argue the even deeper problem is that it's implementation defined. Should be in the spec and they should conform to the spec. That's why I'm so paranoid and zeroize things myself. Too much hassle to remember what is or isn't zero.

homebrewer

Since having multiple compilers is often touted as an advantage, how often do situations like what you're describing happen compared to the opposite — when a second compiler surfaces bugs in one's application or the other compiler?

uecker

I compile my projects with clang and GCC. It is quite often that one compilers finds minor issues the other does not.

iamthejuan

It is like an era of average.

zeroq

i will call it "webification" of C!

mtklein

This was my instinct too, until I got this little tickle in the back of my head that maybe I remembered that Clang was already acting like this, so maybe it won't be so bad. Notice 32-bit wzr vs 64-bit xzr:

    $ cat union.c && clang -O1 -c union.c -o union.o && objdump -d union.o
    union foo {
        float  f;
        double d;
    };

    void create_f(union foo *u) {
        *u = (union foo){0};
    }

    void create_d(union foo *u) {
        *u = (union foo){.d=0};
    }

    union.o: file format mach-o arm64

    Disassembly of section __TEXT,__text:

    0000000000000000 <ltmp0>:
           0: b900001f      str wzr, [x0]
           4: d65f03c0      ret

    0000000000000008 <_create_d>:
           8: f900001f      str xzr, [x0]
           c: d65f03c0      ret

mtklein

Ah, I can confirm what I see elsewhere in the thread, this is no longer true in Clang. That first clang was Apple Clang 17---who knows what version that actually is---and here is Clang 20:

    $ /opt/homebrew/opt/llvm/bin/clang-20 -O1 -c union.c -o union.o && objdump -d union.o

    union.o: file format mach-o arm64

    Disassembly of section __TEXT,__text:

    0000000000000000 <ltmp0>:
           0: f900001f      str xzr, [x0]
           4: d65f03c0      ret

    0000000000000008 <_create_d>:
           8: f900001f      str xzr, [x0]
           c: d65f03c0      ret

dzaima

Looks like that change is clang ≤19 to clang 20: https://godbolt.org/z/7zrocxGaq

ogoffart

> This is going to silently break so much existing code

The code was already broken. It was an undefined behavior.

That's a problem with C and it's undefined behavior minefields.

ryao

GCC has long been known to define undefined behavior in C unions. In particular, type punning in unions is undefined behavior under the C and C++ standards, but GCC (and Clang) define it.

mtklein

I have always thought that punning through a union was legal in C but UB in C++, and that punning through incompatible pointer casting was UB in both.

I am basing this entirely on memory and the wikipedia article on type punning. I welcome extremely pedantic feedback.

flohofwoe

> type punning in unions is undefined behavior under the C and C++ standards

Union type punning is entirely valid in C, but UB in C++ (one of the surprisingly many subtle but still fundamental differences between C and C++). There's specifically a (somewhat obscure) footnote about this in the C standard, which also has been more clarified in one of the recent C standards.

mat_epice

EDIT: This comment is wrong, see fsmv’s comment below. Leaving for posterity because I’m no coward!

- - -

Undefined behavior only means that the spec leaves a particular situation undefined and that the compiler implementor can do whatever they want. Every compiler defines undefined behavior, whether it’s documented (or easy to qualify, or deterministic) or not.

It is in poor taste that gcc has had widely used, documented behaviors that are changing, especially in a point release.

grandempire

When you have a big system many people rely on you generally try to look for ways to keep their code working - not look for the changes you’re contractually allowed to make.

GCC probably has a better justification than “we are allowed to”.

arp242

> GCC probably has a better justification than “we are allowed to”.

Maybe, but I've seen GCC people justify such changes with little more than "it's UB, we can change it, end of story", so I wouldn't assume it.

mwkaufma

Undefined in the standard doesn't mean undefined in GCC. Type-punning through unions has always been a special case that GCC has taken care with beyond the standard.

myrmidon

I honestly feel that "uninitialized by default" is strictly a mistake, a relic from the days when C was basically cross-platform assembly language.

Zero-initialized-by-default for everything would be an extremely beneficial tradeoff IMO.

Maybe with a __noinit attribute or somesuch for the few cases where you don't need a variable to be initialized AND the compiler is too stupid to optimize the zero-initialization away on its own.

This would not even break existing code, just lead to a few easily fixed performance regressions, but it would make it significantly harder to introduce undefined and difficult to spot behavior by accident (because very often code assumes zero-initialization and gets it purely by chance, and this is also most likely to happen in the edge cases that might not be covered by tests under memory sanitizer if you even have those).

rwmj

GCC now supports -ftrivial-auto-var-init=[zero|uninitialized|pattern] for stack variables https://gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html#ind...

For malloc, you could use a custom allocator, or replace all the calls with calloc.

myrmidon

Very nice, did not know about this!

The only problem with vendor extensions like this is that you can't really rely on it, so you're still kinda forced to keep all the (redundant) zero intialization; solving it at the language level is much nicer. Maybe with C2030...

bjourne

There are many low-level devices where initialization is very expensive. It may mean that you need two passes through memory instead of one, making whatever code you are running twice as slow.

myrmidon

I would argue that these cases are pretty rare, and you could always get nominal performance with the __noinit hint, but I think this would seldomly even be needed.

If you have instances of zero-initialized structs where you set individual fields after the initialization, all modern compiler will elide the dead stores in the the typical cases already anyway, and data of relevant size that is supposed to stay uninitialized for long is rare and a bit of an anti-pattern in my opinion anyway.

modeless

Ok, those developers can use a compiler flag. We need defaults that work better for the vast majority.

nullc

meh, the compiler can almost always eliminate the spurious default initialization because it can prove that first use is the variable being set by the real initialization. The only time the redundant initialization will be emitted by an optimizing compiler is when it can't prove its redundant.

I think the better reason to not default initialize as a part of the language syntax is that it hides bugs.

If the developers intent is that the correct initial state is 0 they should just explicitly initialize to zero. If they haven't, then they must intend that the correct initial state is the dynamic one in their code and the compiler silently slipping in a 0 in cases the programmer overlooked is a missed opportunity to detect a bug due to the programmer under-specifying the program.

nullc

Zero initializing often hides real and serious bugs, however. Say you have a function with an internal variable LEN that ought to get set to some dynamic length that internal operations will run over. Changes to the code introduce a path which skips the setting of LEN. Current compilers will (very likely) warn you about the potentially uninitialized use, valgrind will warn you (assuming the case gets triggered), and failing all that the program will potentially crash when some large value ends up in LEN-- alerting you to the issue.

Compare with default zero init: The compiler won't warn you, valgrind won't warn you, and the program won't crash. It will just be silently wrong in many cases (particularly for length/count variables).

Generally the attention to exploit safety can sometimes push us in directions that are bad for program correctness. There are many places where exploit safety is important, but also many cases where its irrelevant. For security it's generally 'safe' is a program erroneously shuts down or does less than it should but that is far from true for software generally.

I prefer this behavior: Use of an uninitialized variable is an error which the compiler will warn about, however, in code where the compiler cannot prove that it is not used the compiler's behavior is implementation defined and can include trapping on use, initializing to zero, or initializing to ~0 (the complement of zero) or other likely to crash pattern. The developer may annotate with _noinit which makes any use UB and avoids the cost of inserting a trap or ~0 initialization. ~0 init will usually fail but seldom in a silent way, so hopefully at least any user reports will be reproducible.

Similar to RESTRICT _noinit is a potential footgun, but its usage would presumably be quite rare and only in carefully maintained performance critical code. Code using _noinit like RESTRICT is at least still more maintainable than assembly.

This approach preserves the compiler's ability to detect programmer error, and lets the implementation pick the preferred way to handle the remaining error. In some contexts it's preferable to trap cleanly or crash reliably (init to ~0 or explicit trap), in others its better to be silently wrong (init 0).

Since C99 lets you declare variables wherever so it is often easy to just declare a variable where it is first set and that's probably best, of course. .. when you can.

bluGill

C++26 has everything initialiied by default. The value is not specified though. Implementations are encourage to use something weird to detect using before explict initialization.

elromulous

Devil's advocate: this would be unacceptable for os kernels and super performance critical code (e.g. hft).

TuxSH

> this would be unacceptable for os kernels

Depends on the boundary. I can give a non-Linux, microkernel example (but that was/is shipped on dozens of millions of devices):

- prior to 11.0, Nintendo 3DS kernel SVC (syscall) implementations did not clear output parameters, leading to extremely trivial leaks. Unprivileged processes could retrieve kernel-mode stack addresses easily and making exploit code much easier to write, example here: https://github.com/TuxSH/universal-otherapp/blob/master/sour...

- Nintendo started clearing all temporary registers on the Switch kernel at some point (iirc x0-x7 and some more); on the 3DS they never did that, and you can leak kernel object addresses quite easily (iirc by reading r2), this made an entire class of use-after-free and arbwrite bugs easier to exploit (call SvcCreateSemaphore 3 times, get sema kernel object address, use one of the now-patched exploit that can cause a double-decref on the KSemaphore, call SvcWaitSynchronization, profit)

more generally:

- unclearead padding in structures + copy to user = infoleak

so one at least ought to be careful where crossing privilege boundaries

myrmidon

No, just throw the __noinit attribute at every place where its needed.

You probably would not even need it in a lot of instances because the compiler would elide lots of dead stores (zeroing) even without hinting.

sidkshatriya

Would you rather have a HFT trade go correctly and a few nanoseconds slower or a few nanoseconds faster but with some edge case bugs related to variable initialisation ?

You might claim that that you can have both but bugs are more inevitable in the uninitialised by default scenario. I doubt that variable initialisation is the thing that would slow down HFT. I would posit is it things like network latency that would dominate.

pjmlp

It is acceptable enough for Windows, Android and macOS, that have been doing for at least the last five years.

That is the usual fearmongering when security improvements are done to C and C++.

saagarjha

The same OS kernel that zeros out pages before handing them back to me?

null

[deleted]

zzo38computer

I thought that {} should always initialize everything regardless of whether or not there is anything in between the braces, and that {0} should only be valid if the first member is a numeric or pointer type (but otherwise has the same effect as {} with nothing in between). I thought that would make more sense, isn't it?

(If you write {} with multiple values when initializing a union, then it should be an error unless all of the values are the same and all of the corresponding members (the first few if you do not explicitly specify which ones) are of the same type as each other.)

wahern

C never had {} until C23. In C {0} was the only way to explicitly zero-initialize a structure in a generic manner. It works because in C initializer lists are applied to members as-if nested structures are flattened out lexically.

However, a long time ago C++ went in a completely different direction with initializer lists, and gcc and clang started emitting warnings (in C mode) about otherwise perfectly valid C code, thus the adoption of C++'s {} for C23. {0} is still technically valid C23, though, as well as valid C89, C90, C99, and C11. In fact, reading both C23 and C89 I'm struck by how little the language has changed:

C89 3.5.7p16:

> If the aggregate contains members that are aggregates or unions, or if the first member of a union is an aggregate or union, the rules apply recursively to the subaggregates or contained unions. If the initializer of a subaggregate or contained union begins with a left brace, the initializers enclosed by that brace and its matching right brace initialize the members of the subaggregate or the first member of the contained union. Otherwise, only enough initializers from the list are taken to account for the members of the first subaggregate or the first member of the contained union; any remaining initializers are left to initialize the next member of the aggregate of which the current subaggregate or contained union is a part.

C23 6.7.10p21:

> If the aggregate or union contains elements or members that are aggregates or unions, these rules apply recursively to the subaggregates or contained unions. If the initializer of a subaggregate or contained union begins with a left brace, the initializers enclosed by that brace and its matching right brace initialize the elements or members of the subaggregate or the contained union. Otherwise, only enough initializers from the list are taken to account for the elements or members of the subaggregate or the first member of the contained union; any remaining initializers are left to initialize the next element or member of the aggregate of which the current subaggregate or contained union is a part.

Blikkentrekker

I have to say, I've read the discussion this generated and it's a bit scary how no one seems to know whether type punning through unions is undefined or not in C, or rather, my conclusion reading it all is more so that many people are wrong and that is defined behavior, but some of the people who are wrong about it are actual GCC compiler developers so it can't be too easy to be right.

krackers

I don't understand why newer revisions of C don't work on fixing these small issues. Things that were previously "undefined/implementation-defined behavior" can easily be made to behave sensibly without breaking anything. Type punning, 2s complement overflow, 0-initializtion of unions, all of those should "just behave" sensibly how the programmer expects. And you can already get there with the right compiler flags, so why not just codify it. It's also not going to break anything since it was undefined behavior in the first place.

Blikkentrekker

The thing about writing standards is that if you write standards compiler writers vehemently disagree with, they will just not implement them, and they disagree with it because their consumers do. A standard typically documents what is already happening. This is why some languages call their standards “reports”. They investigate and document what the majority of compilers are currently doing and encourage the others to follow suit.

As for overflow, the reality is that most compilers simply assume it won't happen at this point. They do this because the consumers want it because it simply generates far faster code being able to assume that it won't happen. Yes, people often come with pathological examples to show why this is a bad idea of ridiculous optimizations being made no one expects because compilers assume it won't ever happen, but those are pathological, in practice it really comes down to loops. In many loops, compilers having to assume that loop variables can overflow in theory disables all sorts of optimizations and elisions and in practice they won't overflow and if they overflow that's an unintended bug anyway.

Obviously a a very basic example is a loop adding some counter value to a counter and stopping when the counter is past a certain value. Assuming that integers can overflow, and that thus adding a value can make the counter less than what it used to be in theory obviously disables many optimizations in streamlining the logic. Just in general, assuming overflow can't occur means being able to make the assumption that adding a positive integer to another integer will always produce a larger integer than the original, that is a very powerful assumption for optimizations to be able to make obviously, assuming that overflow can happen removes it that's why it's undefined behavior. Compilers are free to assume it will never happen.

darthwalsh

C still supports a huge variety of embedded processors, which I imagine influences the overflow UB. But clearing up the type semantics would be nice.

mastax

Do distros have tooling to deal with this type of change?

I imagine it would be very useful to be able to search through all the C/C++ source files for all the packages in the distro in a semantic manner, so that it understands typedefs and preprocessor macros etc. The search query for this change would be something like "find all union types whose first member is not its largest member, then find all lines of code where that type is initialized with `{0}`".

ryao

As a retired Gentoo developer, I can say not really as far as I know. There could be static analysis tools that can find this, but I am not aware of anyone who runs them on the entire distribution.

mastax

In theory it's just an extension of IDE tooling. A CLI with a little query language wrapping libclang. In practice I'm sure it's a nightmare just to get 20,000 packages' build systems wrangled such that the right source files get indexed by libclang, and all the endless plumbing for downloading packages and reporting results, and on and on.

ris

Distributions tend to use shell-script-wrapped compilers that can inject additional flags desired by the distribution, and in all likelihood distributions will just add flags that force the old behaviour if there are problems.

anon-3988

lol this is exactly the kind of stuff I expects from C or C++ haha its kinda insane people just decide to do this amidst all the talk about correctness/safety.

omoikane

Really excited about #embed support:

> C: #embed preprocessing directive support.

> C++: P1967R14, #embed (PR119065)

See also:

https://news.ycombinator.com/item?id=32201951 - Embed is in C23 (2022-07-23)

NekkoDroid

I'd really wish for an `std::embed<...>` that would be a consteval function (IIRC there is a proposal for this, but I don't know its status). The less pre-processor stuff going on the less there is to worry about, the syntax would end up much cleaner and you can create your own wrapper functions.

elvircrn

"C++ Modules have been greatly improved."

It would be nice to know what these great improvements actually are.

canucker2016

Later in the article, it mentions:

    Improved experimental support for C++23, including:

        std and std.compat modules (also supported for C++20).
From https://developers.redhat.com/articles/2025/04/24/new-c-feat...:

    The next major version of the GNU Compiler Collection (GCC), 15.1, is expected to be released in April or May 2025.

    GCC 15 greatly improved the modules code. For instance, module std is now supported (even in C++20 mode).

boris

In GCC 14, C++ modules were unusable (incomplete, full of bugs, no std modules, etc). I haven't tried 15 yet but if that changed, then it definitely qualifies for a "great improvement".

bluGill

Still no std modules but otherwise likely useable. modules are ready for early adoptors to use and start writing the books on what you should do. (Not how to do it, those books are mostly written though not in print. How hou should as is was imbort std a good idea or shoule containers and algorithms been split - or maybe something I haven't though of)

artemonster

those were the greatest improvements of all time. all of them. :D

pjmlp

Interesting to see some improvements being done to Modula-2 frontend as well.

codr7

Finally, musttail, can't wait to try that out.

fithisux

Any Hope for HaikuOs + Winlibs. GDC would be greatly appreciated.