Seriously. People need to stop thinking UB is about allowing different behavior.
It is absolutely not. No amount of arguing changes this.
UB is not implementation defined behavior. That term already exists.
UB places no constraints on the compiler. If the standard does not define the result of doing X in condition Y is undefined, or it just does not define the result (either explicitly, ID, unspecified, or erroneous), then there are no constraints on the compiler.
It does not matter what your implementation, platform, or hardware do. If the behavior is UB according to the language, the compiler can do anything, or nothing, or different things in different places. If the language says it is UB, it is UB.
If you want different implementations to be able to provide different behavior, you want implementation defined behavior. That is literally why ID exists separately from UB.
some people insisted that it’s demonstrably fine, we know what the code will do, and at worst we’ll get a segfault. This is false.
UB places no constraints on the compiler.
I think it comments like yours and posts like this contribute to an extremely negative sentiment around the state of undefined behaviour in languages like C and C++. I don't mean this to imply skepticism or suggest blame, but to state, as neutrally as I can, that examples like those shared in the article are genuinely alarming, and the consequence is negativity.
(The “falling through a function” example comes up a lot, probably because it is very simply yet very provocative, though I think -fsanitize=unreachable may handle this problem, judging from its effect on the Godbolt example the author provides. There appear to be other flags I can use to catch other, similar examples, and while these may be worth mentioning, I don't know that they materially change the conclusions of the article.)
I think this negativity results in some wondering whether the compiler folks have gone mad, but in previous discussions of undefined behaviour, comments from folks like ~david_chisnall or ~lonjil suggest that manifestations of undefined behaviour are not a result of some negligence or some malicious intent.
e.g.,
it is impossible to write an optimizing compiler that behaves sensibly in the presence of UB. The only real solution, which pretty much every programming language in the world other than C and C++ do, is to make it harder or even impossible to have UB in your code.
As an aside, when authors like ~matklad discuss memory safety, they consider undefined behaviour in their definitions, though this leads to conclusions that I'm not sure I agree with, such as:
This is useless! Here, I’ve just made a language called Lil-C, which exactly like C, except that every UB is formally defined to trap. It is safe! And it can run any C program! Have I just done anything useful? No!
This actually seems like it would be quite useful…?
Anyway, by this post and the above comment's reasoning, it is not possible to constrain the consequence of undefined behaviour; as a result, it is not possible to retroäctively detect that it has performed an undesirable transformation. However, since undefined behaviour arises as an emergent property across multiple (desirable, reasonably optimisations,) I also cannot simply read through a lists of undefined behaviours in C and expect to be able to avoid triggered it intentionally. Understandably frustrating!
But, as a human being, I am allowed (if not expected) to have contradictory desires, and to want all benefit with no sacrifice. I want my programming language to be extremely simple, but also extremely fast, but also very safe, but also without undefined behaviour, but also without giving up optimisations that other languages (less safe or more rife with undefined behaviour) are allowed, and I might be convinced not to write any new codes in C or C++ (in fact, I have found myself running “toward” Zig more than “away” from C,) but I definitely want to make substantive changes to existing codes in C and C++ and refuse to replace or rewrite them. (Regarding this last desire, you know, I figure I already dug the hole… and I'm not going to lose sleep over digging it just a little bit deeper…)
So, rather than simply being negative about undefined behaviour or being negative about C and C++ code (that I refuse to do much of anything about…,) perhaps what would be useful to know would be how I can actually go about triangulating myself. I know I'm in a hole, I know I am digging it deeper, I know I won't climb out, and I know I might hit a pipe… but before the water rises up to my neck, is there no agency I can have in my last moments?
In other words, even if I won't rewrite my C codes, and even if I do want to make substantive changes to them, and even if I know I will suffer from the consequences of undefined behaviour, and even if these will be difficult to avoid through additional mechanism or additional diligence, I still have some obligation-to and corresponding freedom-of action. What do I do in the next (… maybe the last? … hopefully the last?) hundred thousand lines of C that I write? Just flail and drown?
I think it comments like yours and posts like this contribute to an extremely negative sentiment around the state of undefined behaviour in languages like C and C++.
And I think that’s a reasonable reaction. Even the C committee is at last working to eliminate as much undefined behaviour from the standard as possible, because everyone agrees the current situation is intolerable.
undefined behaviour arises as an emergent property across multiple (desirable, reasonably optimisations,)
No, that’s backwards. UB was introduced to cope with corners of the spec which were too difficult to pin down, because of divergent hardware behaviour, or because someone might want to enter a program on punched cards, or because it’s just too hard to determine what havoc a wild pointer might cause, etc. It wasn’t until years later that compilers started exploiting UB to make optimizations that would be unsound according to the original intended purpose of UB. UB is part of the language specification, not a result of compiler optimizations.
What do I do in the next (… maybe the last? … hopefully the last?) hundred thousand lines of C that I write?
Study Annex J, crank up the warnings, write thorough tests, use sanitizers, use static analysis.
It wasn’t until years later that compilers started exploiting UB to make optimizations that would be unsound according to the original intended purpose of UB.
If it's genuinely the case that the compiler folks have just gone mad, it makes some of this easier to stomach, but I'm not sure that it really changes much. Presumably, the compiler authors were under pressure to deliver ever-increasing units of performance, and this overrode any reluctance they may have felt toward exploiting undefined behaviour, but that suggests this could happen to anyone…
UB is part of the language specification, not a result of compiler optimizations.
What is considered undefined behaviour is part of the language specification, sure. There is, after all, the old joke about the SIMPLE programming language.
However, it is not the presence of undefined behaviour that is of concern but the byzantine behaviour that arises as its consequence.
Even the C committee is at last working to eliminate as much undefined behaviour from the standard as possible, because everyone agrees the current situation is intolerable.
Study Annex J, crank up the warnings, write thorough tests, use sanitizers, use static analysis.
I think this is why, as an alternative to just crying one's self to sleep every night, naïvely trapping on any undefined behaviour (e.g., via mechanisms like Fil-C) seem to be attracting attention, even in the absence of the C committee taking any action.
If it's genuinely the case that the compiler folks have just gone mad
They haven't gone mad, it goes something like
Does the syntax allow a programmer to express <edge case>? Yes.
Does the language define the semantics of that <edge case>? No.
Ok, assume it never happens and any occurance is programmer error. "You shouldn't do that" is sometimes a fine answer to "what if I do this"
Now optimizations combine in spooky and unpredictable ways sometimes. Like a particular rewrite of an IR may open up new opportunities for inlining or constant folding, and now a handful of very sane optimizations look like the compiler devs went crazy "exploiting" UB. Making decisions assuming the absence of behavior is something we do all the time.
naïvely trapping on any undefined behaviour (e.g., via mechanisms like Fil-C) seem to be attracting attention
The problem is that this is equivalent to removing optimizations and increasing complexity in the design. You need to detect the presence of something that by definition has no presence in correct programs then emit instructions for it that are carried through optimization passes, linking, and loading.
Where I think the gap lies is that UB is supposed to be things that are so unreasonable in correct programs that no compiler could reasonably define a check for it. But C is filled with UB that's easy to detect and reject, which is why the language is moving to define previously undefined behavior (usually as compile errors).
Study Annex J, crank up the warnings, write thorough tests, use sanitizers, use static analysis.
I read and write C for my day job, literally hours every day, have for years. This is how you do it. I'm still a little baffled by how much, uhh, discourse there is about C and it's perceived shortcomings. Not that I think UB is fine, or C at the edges is easy, or anything else. Rather, it's like, if that's the tool you're using, by choice or otherwise, why wouldn't you learn and do everything you can to maximise its strengths and minimise its weaknesses?
My entire point is that UB is not about supporting different platforms, that "implementation defined behavior" is a term that exists in both the C and C++ standards specifically to say "this operation has defined behavior, and the implementation (compiler, platform, etc) defines that behavior".
The big problem is when people apply "UB is for cross platform support", and then treat UB operations as being ok because they know the platform that they are targeting. Which it isn't.
If you want to make a difference, write proposals to WG16(C) and/or WG21(C++), to make platform defined operations be specified as having defined behavior (defined, implementation defined, unspecified, erroneous) rather than being UB.
Your complaint is ... you agree that UB has the qualities outlined by TFA, ~olliej and others, but you think the comments should be more positive somehow?
What you can do is: learn C. Learn what the standard guarantees, what choices it leaves up to implementations (implementation-defined behavior) and what it labels undefined behavior. Then try to avoid writing code which invokes undefined behavior. That's it. Don't do what so many wants to do and dismiss it because "I only target one platform" because it's not that simple.
Here's a few notes about types and aliasing:
Every object has an "effective type". The effective type of stack-allocated objects have the type you declared them as (e.g an int foo has effective type of int, a struct sockaddr_in addr has the effective type of struct sockaddr_in).
Heap-allocated memory starts out without an effective address (yes, malloc is magic; you may treat memory obtained from mmap the same way). They gain an effective type the moment you write anything to it (either by writing through a pointer or through functions like memcpy).
Do not dereference an object of effective type X through a pointer type of type Y. If you have an int, don't dereference it through a pointer to float. There are two exceptions:
You can look at the memory of any object by going through a char * or unsigned char *. This is how e.g memcpy can copy bytes without knowing the effective type.
Note that this does not work the other way: if you have an object of effective type char (e.g as a part of a stack-allocated array char[N]), you may not access it through a pointer to some other type. This has implications for how you can use stack-allocated buffers.
If you have two struct types X and Y with a "common initial sequence" (meaning the first few fields are the same types in the same order), you may have an object with effective type X and look at those common initial fields through a pointer to Y.
The consequence of this is that you should be using memcpy a lot more; if you have some memory buffer and you know there's an int at offset K, don't make an int * to it, instead make a local int variable and memcpy the data. The compiler will figure out how to make it fast.
Then there are the obvious ones: don't dereference null pointers, don't overflow integers (not even through addition or bit shifting), don't write outside of memory you allocated.
There's more of course, this is just what I remember off the top of my head right now as things which might catch people off guard if they learned about C by learning that "memory is a big flat array of bytes and pointers are indexes into that array". That is true when you're implementing C but not when you're programming in C.
Then there are kinds of UB which you can safely ignore for a given platform. For example, ISO C disallows casting between an object pointer (such as void *) and a function pointer; however the POSIX dynamic linker API provides one function dlsym which returns a void *, which the caller is expected to cast to a function pointer type and then call. As a result, every implementation which supports the POSIX dynamic linker API allows casting between object pointers and function pointers. And both GCC and Clang (and probably others) explicitly allow you to violate the effective type rules by going through a union.
yes, malloc is magic; you may treat memory obtained from mmap the same way
As for how you can justify and think about this kind of move in terms of the existing abstract machine semantics of malloc, I really like @ralfj's exposition at https://www.ralfj.de/blog/2026/03/13/inline-asm.html. I believe a possible formalization of storytelling is something like the FFI oracle in CakeML's semantics. Although a language like CakeML doesn't have to contend with the complexity of a C-like or Rust-like memory model, the idea should generalize to whenever you're trying to bridge two alien worlds and you can explain the locally visible effect of the remote world in terms of local semantics. What seems hard is coming up with the right explanations for tricky examples, as in Ralf's article. There's a concept I'm originally familiar with from linearizability where you're allowed to explain a final set of observations retroactively, which allows for and often requires similar creativity.
Please do! I "know" all of these things but rarely think about them, just try to remember them when the spidey sense tingles. A moment to read and reflect away from actually writing code helps a lot!
There are two kinds of underlined behaviour in C and C++.
The first is for things where there is not necessarily a consistent delivery possible in an efficient implementation. For example, consider division by zero. Different ISAs behave differently on division by zero. Okay, you might think, make it implementation defined and let implementations define it as ‘whatever the CPU does on a divide instruction’. That’s a nice idea except that depends on a bunch of things that might not be true:
The ISA spec must define it (in some ISAs, the destination register is define to have an unspecified value and the exact behavior varies depending on implementations).
It must be consistent and enumerable. On some ISAs, you may or may not get a trap depending on the state of some register. On UNIX-like platforms on x86 raise SIGFPE on division by zero, unless the floating-point environment is defined to not trap, in which case they resume execution after the divide with whatever value was in the target register before the instruction, so the defined semantics are ‘you will get a trap or an arbitrary value’.
it must be consistent across lowerings. Consider autovectorisation. If you do x / y in a loop and the loop is vectorised. Is the behaviour consistent between a scalar divide and a vector divide?
C++ recently introduced a concept of Erroneous Behaviour (EB) which says, roughly: don’t do this. If you do, you may get unspecified values, but the value will be stable. Not the same each time, necessarily, but if you use the value twice then it will be the same value. The first place this is used is for uninitialised variables on the stack. Consider the following:
bool x;
if (x) foo();
if (!x) bar();
In C++, the read of x is now EB. The standard mandates that eitherfoo or ‘bar will be called, but does not specify which. It must be *exactly* one of them because the first conditional read the variable and subsequent reads must give the same result. The *next* time that you reach this code and ‘x reenters scope, the other function may be called. Ideally, a compiler will warn about this.
There’s ongoing work to constrain more of this kind of UB in ways that doesn’t impede optimisation. Division by zero is hard because of the vectorisation question. If a new version of the standard gave a 4x slowdown for performance-critical code, people would object loudly.
The other kind is more interesting. It is behaviour that should not happen but cannot be statically proven to not occur. The canonical example of this is use-after-free. Consider this contrived example:
int increment_and_clamp(int *array, int length, int threshold, int *max)
{
for (int i=0 ; i<length ; i++)
{
array[i]++;
if (array[i] > threshold)
{
*max = std::max(*max, array[i]);
array[i] = threshold
}
}
return *max;
}
A compiler may wish to hoist the initial load of *max out of the loop and move the final store to the end. This is permitted in the C++ abstract machine (and C if you replace std::max with a C equivalent) because nothing in this function establishes a happens-before edge, so the order in which these stores are visible to other threads is unspecified.
Now consider that the max is a dangling pointer and accessing it traps. As written, this means that all of the array up to the first one above the threshold will be incremented and then the trap will happen. Assume this thread dies, other threads will see the partially incremented array. But after optimisation, the function will trap on entry.
Now consider a slightly more complex transform that vectorises the loop. Now the trap could happen in the middle of an operation, interleaved between source-level loop iterations.
The same applies to the rule that says dereferencing null is undefined. If the compiler must trap on null dereferences at the precise place in program order that they occur, this significantly constrains the set of permitted optimisations. Ensuring that max is not a null or dangling pointer is, in the general case, not computable. It is a whole-program dataflow property. If this is a public function, the C/C++ compilation model ensures that the compiler cannot see all callers so cannot even guarantee this in some trivial cases unless you do whole-program LTO (and give up on shared libraries).
This is where languages like Rust are interesting. A Rust compiler can generate the same LLVM IR as a C compiler for an example like this. The mid-level optimisers can, in both cases, assume that max is a valid pointer for the duration of the call and perform the same transforms. The difference is that the Rust type system and borrow checking statically guarantee this with checks performed before generation of the IR (and will reject programs where this cannot be statically proven, which is a superset of programs where it is not true). The C compiler guarantees this via a mechanism known informally as ‘trust me, bro’.
An implementation is allowed to provide a definition for what the C standard leaves implicitly or explicitly undefined. There are also standards, such as POSIX, that themselves define some things which standard C leaves undefined.
This is not to say that all UB is like this, or that all people who write C and introduce UB into their code know what they're doing, but some people do know what they're doing.
If you are writing C and you are writing C against one of these other standards, or specifically against a specific implementation, there is nothing wrong with relying on the specific explicit definitions of behaviour which may otherwise be undefined, as long as you're doing it consciously.
(I maintain, however, that nobody that is looking for advice on whether they should write C should not be writing C. The people who are writing C and are not looking or advice on whether they should continue to write it should also seriously consider moving to something with fewer footguns if they can.)
An implementation is allowed to provide a definition for what the C standard leaves implicitly or explicitly undefined. There are also standards, such as POSIX, that themselves define some things which standard C leaves undefined.
"leaves undefined" is not what UB means at all is sometimes what UB means
So, can anyone articulate precisely what is the difference between the two? Is it really the fact that there's two flavors of "undefined", as in "just" behavior that the C standard had left undefined and UB?
I really cannot think about how, precisely, is the ontological difference between the two supposed to look.
“leaves undefined” is a subset of undefined behaviour. The C standards say:
If a “shall” or “shall not” requirement that appears outside of a constraint or runtime-constraint is violated, the behavior is undefined. Undefined behavior is otherwise indicated in this document by the words “undefined behavior” or by the omission of any explicit definition of behavior. There is no difference in emphasis among these three; they all describe “behavior that is undefined”.
This is an example of UB existing to cope with corners of the spec which are too difficult to pin down, as I mentioned in an earlier comment.
There are also standards, such as POSIX, that themselves define some things which standard C leaves undefined.
And that does not matter.
I C says an API is UB if used in a specific way, it does not matter what the API implementation does or says. If the C standard says that is UB, it puts no requirements on the compiler.
There is no difference between using an API that posix says is defined but C says is not vs dividing by zero, or overflowing an integer.
If you write C code that depends on implementation defined behavior (POSIX), but C says is UB, your C code is wrong.
The fact that you can get away with it today, doesn't magically make your code correct. It does not mean that it will work if you change header orders, or inlining changes, or anything else.
If your code invokes UB according to the language, you are not writing conforming C. Maybe your compiler allows it (for example -fwrapv, -ftrapv, -fno-delete-null-pointer-checks, etc) but that is no longer conforming C, and your program is wrong vs the standard: any other C compiler is free to assume your UB code does not occur, or is unreachable, etc.
So nobody should program for POSIX in C then? I can't use dlopen() because it mandates that a void * can store a function pointer? I can't use memset() on a structure with pointer fields and assume they're now NULL?
To a point, yes, those violate the C standard, but if I'm targeting POSIX, I have less UB do worry about than pure C (not much, but some).
I can't use dlopen() because it mandates that a void * can store a function pointer?
You're right, much like signed integer overflow, it often works. It's also not correct C.
I can't use memset() on a structure with pointer fields and assume they're now NULL?
You can, that's what Implementation Defined behavior is for.
To a point, yes, those violate the C standard, but if I'm targeting POSIX, I have less UB do worry about than pure C (not much, but some).
This is literally my point. You can't say "I am targeting POSIX and therefore what the standard says is not relevant". If C says X is UB, it is UB. Being able to get away with it does not make it non-UB, and it does not make your code correct.
For many many years people relied on null dereferences as having safe termination semantics, and then compilers said "well that's UB" and suddenly all that incorrect code became insecure incorrect code.
If your code relies on UB, your code is wrong according to the standard. If a compiler chooses to, that incorrect code can become a symptomatic bug in future.
Getting away with UB today does not change anything.
Let's look at your first point: void* cannot hold a function pointer, that's UB. Code that stores a function pointer in a void* is wrong. What it should be is implementation defined behavior, but like so many examples of implementation defined behavior in C and C++, it is labeled as UB.
If you want to store function pointers in a void*, and have your code be correct, you need the C committee to make it implementation defined behavior. Or you can join the linux kernel and acknowledge that standard C cannot be used for systems programming due to it incorrectly labeling implementation defined behavior as UB.
Anyway, my point is simple: UB does not enable different platform behaviors, it mean any code relying on that implementation defined behavior is wrong, even when a platform does define the behavior.
You're right, much like signed integer overflow, it often works. It's also not correct C.
I disagree on this one. UB does not say that the implementation must do unspecified things, it says that the C standard places no constraints on what will happen. In this specific case, the POSIX standard places additional constraints on what may happen and it’s safe. This means that there are some C targets that can never support POSIX, but if you’re writing POSIX C then taking advantage of those definitions is fine.
If C says X is UB, but Clang, GCC, MSVC, TCC, Fil-C and all other implementations I care about defines the behavior of X to something sane, then doing X is fine. It's not "correct C" according to the standard but it's correct C according to your implementation. The implementation is allowed to define behavior which the standard leaves undefined.
Treat casting from object pointer to function pointer as a very broadly supported extension to the language.
This is a special case, it's not the case for most forms of UB. I don't want to leave people with the impression that "UB is fine". It's usually not, it just happens to be for this specific kind of UB.
They're technically correct in the most annoying way. It's not up to the API, you can't just cast from an object pointer to a function pointer "because the API I'm using requires me to do so"; it's up to the implementation. But the POSIX API is so privileged that any implementation which intends for you to write against POSIX, defines casting between object pointer and function pointer.
Now this doesn't necessarily mean that "if you're targeting POSIX you're fine". I could write a new, standards compliant C compiler tomorrow which doesn't guarantee anything about what happens when you cast between object pointer and function pointer. Or I could write a compiler which detects it and aborts the process when you do that. You could still import the musl header files on my implementation and use the POSIX dynamic linker API, and your program would crash when you cast from the void * returned by dlsym to a function pointer.
But you're using the POSIX API through gcc, clang, MSVC, TCC, Fil-C or some other implementation that's made with the intention to support the POSIX API. So you're fine. All implementations you could ever conceivably want to use with POSIX in practice will define that behavior even though ISO declares it undefined.
As for using memset() on a struct with pointers: this is an issue of IB rather than UB. Your implementation has to define a bit pattern for the null pointer, per the standard. By doing memset(&foo, 0, sizeof(foo)) and then checking pointers in foo against NULL (or the null pointer constant 0), you risk invoking UB on implementations which define the null pointer's bit pattern to be something other than all 0s. This isn't a concern if you don't care about extremely niche implementations. On GCC, Clang, MSVC, TCC, Fil-C and all other "normal" C compilers, the null pointer is defined to have an all zero bit pattern, so you never invoke UB.
you risk invoking UB on implementations which define the null pointer's bit pattern to be something other than all 0s. This isn't a concern if you don't care about extremely niche implementations
If you are writing a compiler that uses LLVM as a backend to compile to AMD GPU code, then specifically for "local" memory (I think what nvidia calls "shared" memory), the NULL value is not zero.
Whether this is a concern when writing C code and compiling to AMDGPU using clang, I honestly don't know.
I give up. I do not understand why people responded to my comment justifying why they want to use UB as if my complaint were them doing that. My complaint is very simple: something being UB means an implementation can do whatever it wants. It does not mean that you, the developer, can rely on that behavior.
That's literally what UB means.
I did not, at any point say "you should not be allowed to rely on this".
I said that by labeling such operations as UB, C and C++ said that you are not allowed to.
In every one of these cases, all you need to do is convince the C and C++ committees to just define the behavior. Doing that simply requires replacing "is undefined" with (or actually saying at ll" "is {implementation defined|unspecified|erroneous} behavior." Those changes instantly gives you what you want, rather than what you are actually being given.
Anyway, I'm done talking into a wall of people rejecting the plain language of the spec, and the demonstrable behavior of C and C++ compilers when reasoning about UB operations. Instead I'll focus on actually improving the standards instead.
People respond to your comment in that way because they do exactly what you yourself suggested they do:
Or you can join the linux kernel and acknowledge that standard C cannot be used for systems programming due to it incorrectly labeling implementation defined behavior as UB.
That's what most C programmers, in fact, are doing. They aren't writing standard C, they are writing "POSIX C" or "GNU C" or the intersection of "GNU C" and "MSVC C" or whatever.
I give up. I do not understand why people responded to my comment justifying why they want to use UB as if my complaint were them doing that.
I don't get this response?
&c.
People are responding in that way because it is not clear what, exactly, you are complaining about, except that you are complaining about something and this something involves what everyone else is doing.
It is not helpful to get up on a tribune and shout "you, all, listen here, you all absolutely cannot fly for any reason whatsoever" if people around you are, in fact, routinely flying. Maybe with varying success, maybe some of them are crashing and burning randomly, maybe it all works due to sheer chance (that sheer chance that occurs 19 times out of 20), but they are indeed flying.
Just getting up there and shouting "no, you all cannot do that" is not really helpful simply because it is not clear what, exactly, are you saying and how exactly does it relate to the reality around you.
Sure, if you write an actual std::unreachable() at a point in code that is quite clearly actually reachable, or take a literal null pointer and dereference it, the compiler may generate code that has what could be seen as pretty whack behaviour (and even that is happening less as there's been more focus from the compiler engineers on avoiding it - witness the output of the GCC compiler that is referenced in the actual post). But coders generally don't do these things, because they don't make sense in the first place. I'm not saying these sort of thing absolutely never happen, but using them as examples of how UB can "always" have unpredictable results seems off.
(As an aside, the existence of std::unreachable() is designed to make it easier for the compiler to optimise safely, i.e. it can emit trapping instructions such as ud2 in cases where it sees definite UB on some path, and omit those traps when the code has been marked as expectedly unreachable via std::unreachable(). I.e. there's a reasonable alternative for the compiler to optimise other than by relying on the mere presence of UB to identify paths that won't actually execute. For this reason, programmers need to be especially careful when using std::unreachable(); get it wrong and there really are no compiler-provided safety nets, unlike some other forms of UB where there may be. And already it should be clear that the linked post is vastly oversimplifying things).
Anything involving pointers is pretty prone to horrible consequences if there's UB - while a null pointer dereference typically does manifest in an immediate crash, things like buffer overrun and use-after-free can be much more subtle and much more insidious. This isn't news. It also applies mostly to a very limited subset of causes of UB. Throwing all UB in the same bucket is really misleading, at best. You'll occasionally see people (in reference to just how bad UB is) remark that even just having a missing newline at the end of a source file is UB. They always neglect to mention that it never, ever, actually causes the compiler to do anything significantly different than if the newline was present (beyond perhaps emitting a warning).
I also strongly suspect that the vast majority (I'm saying "vast majority", not all*) of actual runtime UB occurrences that don't involve dereference of invalid pointers are either harmless in effect or at least do exactly what you would expect (eg integer overflow typically wraps if that's what happens on the underlying architecture). The post is needlessly sensationalist, as was the preceding one.
*: notable exceptions include two classics, "the compiler ate my overflow check" and "I logged the value of an uninitialised variable but nothing happened" - even the latter one should cease to be an issue going forward with language changes that have been coming down the pipeline
olliej | 11 hours ago
Seriously. People need to stop thinking UB is about allowing different behavior.
It is absolutely not. No amount of arguing changes this.
UB is not implementation defined behavior. That term already exists.
UB places no constraints on the compiler. If the standard does not define the result of doing X in condition Y is undefined, or it just does not define the result (either explicitly, ID, unspecified, or erroneous), then there are no constraints on the compiler.
It does not matter what your implementation, platform, or hardware do. If the behavior is UB according to the language, the compiler can do anything, or nothing, or different things in different places. If the language says it is UB, it is UB.
If you want different implementations to be able to provide different behavior, you want implementation defined behavior. That is literally why ID exists separately from UB.
dutc | 7 hours ago
I think it comments like yours and posts like this contribute to an extremely negative sentiment around the state of undefined behaviour in languages like C and C++. I don't mean this to imply skepticism or suggest blame, but to state, as neutrally as I can, that examples like those shared in the article are genuinely alarming, and the consequence is negativity.
(The “falling through a function” example comes up a lot, probably because it is very simply yet very provocative, though I think
-fsanitize=unreachablemay handle this problem, judging from its effect on the Godbolt example the author provides. There appear to be other flags I can use to catch other, similar examples, and while these may be worth mentioning, I don't know that they materially change the conclusions of the article.)I think this negativity results in some wondering whether the compiler folks have gone mad, but in previous discussions of undefined behaviour, comments from folks like ~david_chisnall or ~lonjil suggest that manifestations of undefined behaviour are not a result of some negligence or some malicious intent.
e.g.,
In fact, authors like Chris Lattner have even been quite explicit in pointing out “advantages of undefined behaviour in C”.
As an aside, when authors like ~matklad discuss memory safety, they consider undefined behaviour in their definitions, though this leads to conclusions that I'm not sure I agree with, such as:
This actually seems like it would be quite useful…?
Anyway, by this post and the above comment's reasoning, it is not possible to constrain the consequence of undefined behaviour; as a result, it is not possible to retroäctively detect that it has performed an undesirable transformation. However, since undefined behaviour arises as an emergent property across multiple (desirable, reasonably optimisations,) I also cannot simply read through a lists of undefined behaviours in C and expect to be able to avoid triggered it intentionally. Understandably frustrating!
But, as a human being, I am allowed (if not expected) to have contradictory desires, and to want all benefit with no sacrifice. I want my programming language to be extremely simple, but also extremely fast, but also very safe, but also without undefined behaviour, but also without giving up optimisations that other languages (less safe or more rife with undefined behaviour) are allowed, and I might be convinced not to write any new codes in C or C++ (in fact, I have found myself running “toward” Zig more than “away” from C,) but I definitely want to make substantive changes to existing codes in C and C++ and refuse to replace or rewrite them. (Regarding this last desire, you know, I figure I already dug the hole… and I'm not going to lose sleep over digging it just a little bit deeper…)
So, rather than simply being negative about undefined behaviour or being negative about C and C++ code (that I refuse to do much of anything about…,) perhaps what would be useful to know would be how I can actually go about triangulating myself. I know I'm in a hole, I know I am digging it deeper, I know I won't climb out, and I know I might hit a pipe… but before the water rises up to my neck, is there no agency I can have in my last moments?
In other words, even if I won't rewrite my C codes, and even if I do want to make substantive changes to them, and even if I know I will suffer from the consequences of undefined behaviour, and even if these will be difficult to avoid through additional mechanism or additional diligence, I still have some obligation-to and corresponding freedom-of action. What do I do in the next (… maybe the last? … hopefully the last?) hundred thousand lines of C that I write? Just flail and drown?
fanf | 7 hours ago
And I think that’s a reasonable reaction. Even the C committee is at last working to eliminate as much undefined behaviour from the standard as possible, because everyone agrees the current situation is intolerable.
No, that’s backwards. UB was introduced to cope with corners of the spec which were too difficult to pin down, because of divergent hardware behaviour, or because someone might want to enter a program on punched cards, or because it’s just too hard to determine what havoc a wild pointer might cause, etc. It wasn’t until years later that compilers started exploiting UB to make optimizations that would be unsound according to the original intended purpose of UB. UB is part of the language specification, not a result of compiler optimizations.
Study Annex J, crank up the warnings, write thorough tests, use sanitizers, use static analysis.
dutc | 6 hours ago
I know I sure don't feel great about it…
If it's genuinely the case that the compiler folks have just gone mad, it makes some of this easier to stomach, but I'm not sure that it really changes much. Presumably, the compiler authors were under pressure to deliver ever-increasing units of performance, and this overrode any reluctance they may have felt toward exploiting undefined behaviour, but that suggests this could happen to anyone…
What is considered undefined behaviour is part of the language specification, sure. There is, after all, the old joke about the SIMPLE programming language.
However, it is not the presence of undefined behaviour that is of concern but the byzantine behaviour that arises as its consequence.
I believe the argument made in the comments I cited is that the undesirable behaviour is an unavoidable result of desirable optimisations. Somewhat similarly, we might argue that “undefined behavior in programmer-visible abstractions represents an aggressive and dangerous tradeoff: it sacrifices program correctness in favor of performance and compiler simplicity. On the other hand, UB at lower levels of the system, such as machine code or a compiler IR, is an internal design choice that needn’t have any effect on the programmer-facing parts of the system”.
I think this is why, as an alternative to just crying one's self to sleep every night, naïvely trapping on any undefined behaviour (e.g., via mechanisms like Fil-C) seem to be attracting attention, even in the absence of the C committee taking any action.
mikedorf | 3 hours ago
They haven't gone mad, it goes something like
Now optimizations combine in spooky and unpredictable ways sometimes. Like a particular rewrite of an IR may open up new opportunities for inlining or constant folding, and now a handful of very sane optimizations look like the compiler devs went crazy "exploiting" UB. Making decisions assuming the absence of behavior is something we do all the time.
The problem is that this is equivalent to removing optimizations and increasing complexity in the design. You need to detect the presence of something that by definition has no presence in correct programs then emit instructions for it that are carried through optimization passes, linking, and loading.
Where I think the gap lies is that UB is supposed to be things that are so unreasonable in correct programs that no compiler could reasonably define a check for it. But C is filled with UB that's easy to detect and reject, which is why the language is moving to define previously undefined behavior (usually as compile errors).
robn | 2 hours ago
I read and write C for my day job, literally hours every day, have for years. This is how you do it. I'm still a little baffled by how much, uhh, discourse there is about C and it's perceived shortcomings. Not that I think UB is fine, or C at the edges is easy, or anything else. Rather, it's like, if that's the tool you're using, by choice or otherwise, why wouldn't you learn and do everything you can to maximise its strengths and minimise its weaknesses?
olliej | 5 hours ago
I don't get this response?
My entire point is that UB is not about supporting different platforms, that "implementation defined behavior" is a term that exists in both the C and C++ standards specifically to say "this operation has defined behavior, and the implementation (compiler, platform, etc) defines that behavior".
The big problem is when people apply "UB is for cross platform support", and then treat UB operations as being ok because they know the platform that they are targeting. Which it isn't.
If you want to make a difference, write proposals to WG16(C) and/or WG21(C++), to make platform defined operations be specified as having defined behavior (defined, implementation defined, unspecified, erroneous) rather than being UB.
mort | an hour ago
Your complaint is ... you agree that UB has the qualities outlined by TFA, ~olliej and others, but you think the comments should be more positive somehow?
What you can do is: learn C. Learn what the standard guarantees, what choices it leaves up to implementations (implementation-defined behavior) and what it labels undefined behavior. Then try to avoid writing code which invokes undefined behavior. That's it. Don't do what so many wants to do and dismiss it because "I only target one platform" because it's not that simple.
Here's a few notes about types and aliasing:
int foohas effective type of int, astruct sockaddr_in addrhas the effective type ofstruct sockaddr_in).mallocis magic; you may treat memory obtained frommmapthe same way). They gain an effective type the moment you write anything to it (either by writing through a pointer or through functions like memcpy).char *orunsigned char *. This is how e.g memcpy can copy bytes without knowing the effective type.char(e.g as a part of a stack-allocated arraychar[N]), you may not access it through a pointer to some other type. This has implications for how you can use stack-allocated buffers.The consequence of this is that you should be using
memcpya lot more; if you have some memory buffer and you know there's an int at offset K, don't make anint *to it, instead make a local int variable and memcpy the data. The compiler will figure out how to make it fast.Then there are the obvious ones: don't dereference null pointers, don't overflow integers (not even through addition or bit shifting), don't write outside of memory you allocated.
There's more of course, this is just what I remember off the top of my head right now as things which might catch people off guard if they learned about C by learning that "memory is a big flat array of bytes and pointers are indexes into that array". That is true when you're implementing C but not when you're programming in C.
Then there are kinds of UB which you can safely ignore for a given platform. For example, ISO C disallows casting between an object pointer (such as
void *) and a function pointer; however the POSIX dynamic linker API provides one functiondlsymwhich returns avoid *, which the caller is expected to cast to a function pointer type and then call. As a result, every implementation which supports the POSIX dynamic linker API allows casting between object pointers and function pointers. And both GCC and Clang (and probably others) explicitly allow you to violate the effective type rules by going through a union.Maybe I should write a blog post about this...
pervognsen | 58 minutes ago
As for how you can justify and think about this kind of move in terms of the existing abstract machine semantics of malloc, I really like @ralfj's exposition at https://www.ralfj.de/blog/2026/03/13/inline-asm.html. I believe a possible formalization of storytelling is something like the FFI oracle in CakeML's semantics. Although a language like CakeML doesn't have to contend with the complexity of a C-like or Rust-like memory model, the idea should generalize to whenever you're trying to bridge two alien worlds and you can explain the locally visible effect of the remote world in terms of local semantics. What seems hard is coming up with the right explanations for tricky examples, as in Ralf's article. There's a concept I'm originally familiar with from linearizability where you're allowed to explain a final set of observations retroactively, which allows for and often requires similar creativity.
robn | 52 minutes ago
Please do! I "know" all of these things but rarely think about them, just try to remember them when the spidey sense tingles. A moment to read and reflect away from actually writing code helps a lot!
david_chisnall | 30 minutes ago
There are two kinds of underlined behaviour in C and C++.
The first is for things where there is not necessarily a consistent delivery possible in an efficient implementation. For example, consider division by zero. Different ISAs behave differently on division by zero. Okay, you might think, make it implementation defined and let implementations define it as ‘whatever the CPU does on a divide instruction’. That’s a nice idea except that depends on a bunch of things that might not be true:
C++ recently introduced a concept of Erroneous Behaviour (EB) which says, roughly: don’t do this. If you do, you may get unspecified values, but the value will be stable. Not the same each time, necessarily, but if you use the value twice then it will be the same value. The first place this is used is for uninitialised variables on the stack. Consider the following:
In C++, the read of
xis now EB. The standard mandates that eitherfooor ‘barwill be called, but does not specify which. It must be *exactly* one of them because the first conditional read the variable and subsequent reads must give the same result. The *next* time that you reach this code and ‘xreenters scope, the other function may be called. Ideally, a compiler will warn about this.There’s ongoing work to constrain more of this kind of UB in ways that doesn’t impede optimisation. Division by zero is hard because of the vectorisation question. If a new version of the standard gave a 4x slowdown for performance-critical code, people would object loudly.
The other kind is more interesting. It is behaviour that should not happen but cannot be statically proven to not occur. The canonical example of this is use-after-free. Consider this contrived example:
A compiler may wish to hoist the initial load of
*maxout of the loop and move the final store to the end. This is permitted in the C++ abstract machine (and C if you replacestd::maxwith a C equivalent) because nothing in this function establishes a happens-before edge, so the order in which these stores are visible to other threads is unspecified.Now consider that the
maxis a dangling pointer and accessing it traps. As written, this means that all of the array up to the first one above the threshold will be incremented and then the trap will happen. Assume this thread dies, other threads will see the partially incremented array. But after optimisation, the function will trap on entry.Now consider a slightly more complex transform that vectorises the loop. Now the trap could happen in the middle of an operation, interleaved between source-level loop iterations.
The same applies to the rule that says dereferencing null is undefined. If the compiler must trap on null dereferences at the precise place in program order that they occur, this significantly constrains the set of permitted optimisations. Ensuring that
maxis not a null or dangling pointer is, in the general case, not computable. It is a whole-program dataflow property. If this is a public function, the C/C++ compilation model ensures that the compiler cannot see all callers so cannot even guarantee this in some trivial cases unless you do whole-program LTO (and give up on shared libraries).This is where languages like Rust are interesting. A Rust compiler can generate the same LLVM IR as a C compiler for an example like this. The mid-level optimisers can, in both cases, assume that
maxis a valid pointer for the duration of the call and perform the same transforms. The difference is that the Rust type system and borrow checking statically guarantee this with checks performed before generation of the IR (and will reject programs where this cannot be statically proven, which is a superset of programs where it is not true). The C compiler guarantees this via a mechanism known informally as ‘trust me, bro’.atk | 9 hours ago
An implementation is allowed to provide a definition for what the C standard leaves implicitly or explicitly undefined. There are also standards, such as POSIX, that themselves define some things which standard C leaves undefined.
This is not to say that all UB is like this, or that all people who write C and introduce UB into their code know what they're doing, but some people do know what they're doing.
If you are writing C and you are writing C against one of these other standards, or specifically against a specific implementation, there is nothing wrong with relying on the specific explicit definitions of behaviour which may otherwise be undefined, as long as you're doing it consciously.
(I maintain, however, that nobody that is looking for advice on whether they should write C should not be writing C. The people who are writing C and are not looking or advice on whether they should continue to write it should also seriously consider moving to something with fewer footguns if they can.)
chinmay | 8 hours ago
"leaves undefined"
is not what UB means at allis sometimes what UB meansintelfx | 4 hours ago
So, can anyone articulate precisely what is the difference between the two? Is it really the fact that there's two flavors of "undefined", as in "just" behavior that the C standard had left undefined and UB?
I really cannot think about how, precisely, is the ontological difference between the two supposed to look.
fanf | 3 hours ago
“leaves undefined” is a subset of undefined behaviour. The C standards say:
This is an example of UB existing to cope with corners of the spec which are too difficult to pin down, as I mentioned in an earlier comment.
intelfx | 3 hours ago
Right, so GP is just wrong and there is no ontological twist here. Good to know.
chinmay | 3 hours ago
I stand corrected
olliej | 8 hours ago
You cannot write code that depends on UB.
There are also standards, such as POSIX, that themselves define some things which standard C leaves undefined.
And that does not matter.
I C says an API is UB if used in a specific way, it does not matter what the API implementation does or says. If the C standard says that is UB, it puts no requirements on the compiler.
There is no difference between using an API that posix says is defined but C says is not vs dividing by zero, or overflowing an integer.
If you write C code that depends on implementation defined behavior (POSIX), but C says is UB, your C code is wrong.
The fact that you can get away with it today, doesn't magically make your code correct. It does not mean that it will work if you change header orders, or inlining changes, or anything else.
If your code invokes UB according to the language, you are not writing conforming C. Maybe your compiler allows it (for example -fwrapv, -ftrapv, -fno-delete-null-pointer-checks, etc) but that is no longer conforming C, and your program is wrong vs the standard: any other C compiler is free to assume your UB code does not occur, or is unreachable, etc.
spc476 | 6 hours ago
So nobody should program for POSIX in C then? I can't use
dlopen()because it mandates that avoid *can store a function pointer? I can't usememset()on a structure with pointer fields and assume they're nowNULL?To a point, yes, those violate the C standard, but if I'm targeting POSIX, I have less UB do worry about than pure C (not much, but some).
olliej | 5 hours ago
What?
You're right, much like signed integer overflow, it often works. It's also not correct C.
You can, that's what Implementation Defined behavior is for.
This is literally my point. You can't say "I am targeting POSIX and therefore what the standard says is not relevant". If C says X is UB, it is UB. Being able to get away with it does not make it non-UB, and it does not make your code correct.
For many many years people relied on null dereferences as having safe termination semantics, and then compilers said "well that's UB" and suddenly all that incorrect code became insecure incorrect code.
If your code relies on UB, your code is wrong according to the standard. If a compiler chooses to, that incorrect code can become a symptomatic bug in future.
Getting away with UB today does not change anything.
Let's look at your first point: void* cannot hold a function pointer, that's UB. Code that stores a function pointer in a void* is wrong. What it should be is implementation defined behavior, but like so many examples of implementation defined behavior in C and C++, it is labeled as UB.
If you want to store function pointers in a void*, and have your code be correct, you need the C committee to make it implementation defined behavior. Or you can join the linux kernel and acknowledge that standard C cannot be used for systems programming due to it incorrectly labeling implementation defined behavior as UB.
Anyway, my point is simple: UB does not enable different platform behaviors, it mean any code relying on that implementation defined behavior is wrong, even when a platform does define the behavior.
david_chisnall | an hour ago
I disagree on this one. UB does not say that the implementation must do unspecified things, it says that the C standard places no constraints on what will happen. In this specific case, the POSIX standard places additional constraints on what may happen and it’s safe. This means that there are some C targets that can never support POSIX, but if you’re writing POSIX C then taking advantage of those definitions is fine.
mort | 45 minutes ago
If C says X is UB, but Clang, GCC, MSVC, TCC, Fil-C and all other implementations I care about defines the behavior of X to something sane, then doing X is fine. It's not "correct C" according to the standard but it's correct C according to your implementation. The implementation is allowed to define behavior which the standard leaves undefined.
Treat casting from object pointer to function pointer as a very broadly supported extension to the language.
This is a special case, it's not the case for most forms of UB. I don't want to leave people with the impression that "UB is fine". It's usually not, it just happens to be for this specific kind of UB.
mort | 49 minutes ago
They're technically correct in the most annoying way. It's not up to the API, you can't just cast from an object pointer to a function pointer "because the API I'm using requires me to do so"; it's up to the implementation. But the POSIX API is so privileged that any implementation which intends for you to write against POSIX, defines casting between object pointer and function pointer.
Now this doesn't necessarily mean that "if you're targeting POSIX you're fine". I could write a new, standards compliant C compiler tomorrow which doesn't guarantee anything about what happens when you cast between object pointer and function pointer. Or I could write a compiler which detects it and aborts the process when you do that. You could still import the musl header files on my implementation and use the POSIX dynamic linker API, and your program would crash when you cast from the
void *returned by dlsym to a function pointer.But you're using the POSIX API through gcc, clang, MSVC, TCC, Fil-C or some other implementation that's made with the intention to support the POSIX API. So you're fine. All implementations you could ever conceivably want to use with POSIX in practice will define that behavior even though ISO declares it undefined.
As for using
memset()on a struct with pointers: this is an issue of IB rather than UB. Your implementation has to define a bit pattern for the null pointer, per the standard. By doingmemset(&foo, 0, sizeof(foo))and then checking pointers infooagainst NULL (or the null pointer constant0), you risk invoking UB on implementations which define the null pointer's bit pattern to be something other than all 0s. This isn't a concern if you don't care about extremely niche implementations. On GCC, Clang, MSVC, TCC, Fil-C and all other "normal" C compilers, the null pointer is defined to have an all zero bit pattern, so you never invoke UB.tomsmeding | 24 minutes ago
While I don't technically dispute this, look:
https://releases.llvm.org/23.1.0/docs/AMDGPUUsage.html#memory-spaces
If you are writing a compiler that uses LLVM as a backend to compile to AMD GPU code, then specifically for "local" memory (I think what nvidia calls "shared" memory), the NULL value is not zero.
Whether this is a concern when writing C code and compiling to AMDGPU using clang, I honestly don't know.
n3t | 8 hours ago
The first example reminds me of https://kristerw.blogspot.com/2017/09/why-undefined-behavior-may-call-never.html (2017).
olliej | 4 hours ago
I give up. I do not understand why people responded to my comment justifying why they want to use UB as if my complaint were them doing that. My complaint is very simple: something being UB means an implementation can do whatever it wants. It does not mean that you, the developer, can rely on that behavior.
That's literally what UB means.
I did not, at any point say "you should not be allowed to rely on this".
I said that by labeling such operations as UB, C and C++ said that you are not allowed to.
In every one of these cases, all you need to do is convince the C and C++ committees to just define the behavior. Doing that simply requires replacing "is undefined" with (or actually saying at ll" "is {implementation defined|unspecified|erroneous} behavior." Those changes instantly gives you what you want, rather than what you are actually being given.
Anyway, I'm done talking into a wall of people rejecting the plain language of the spec, and the demonstrable behavior of C and C++ compilers when reasoning about UB operations. Instead I'll focus on actually improving the standards instead.
intelfx | 4 hours ago
People respond to your comment in that way because they do exactly what you yourself suggested they do:
That's what most C programmers, in fact, are doing. They aren't writing standard C, they are writing "POSIX C" or "GNU C" or the intersection of "GNU C" and "MSVC C" or whatever.
People are responding in that way because it is not clear what, exactly, you are complaining about, except that you are complaining about something and this something involves what everyone else is doing.
It is not helpful to get up on a tribune and shout "you, all, listen here, you all absolutely cannot fly for any reason whatsoever" if people around you are, in fact, routinely flying. Maybe with varying success, maybe some of them are crashing and burning randomly, maybe it all works due to sheer chance (that sheer chance that occurs 19 times out of 20), but they are indeed flying.
Just getting up there and shouting "no, you all cannot do that" is not really helpful simply because it is not clear what, exactly, are you saying and how exactly does it relate to the reality around you.
davmac | 4 hours ago
Sure, if you write an actual
std::unreachable()at a point in code that is quite clearly actually reachable, or take a literal null pointer and dereference it, the compiler may generate code that has what could be seen as pretty whack behaviour (and even that is happening less as there's been more focus from the compiler engineers on avoiding it - witness the output of the GCC compiler that is referenced in the actual post). But coders generally don't do these things, because they don't make sense in the first place. I'm not saying these sort of thing absolutely never happen, but using them as examples of how UB can "always" have unpredictable results seems off.(As an aside, the existence of
std::unreachable()is designed to make it easier for the compiler to optimise safely, i.e. it can emit trapping instructions such asud2in cases where it sees definite UB on some path, and omit those traps when the code has been marked as expectedly unreachable viastd::unreachable(). I.e. there's a reasonable alternative for the compiler to optimise other than by relying on the mere presence of UB to identify paths that won't actually execute. For this reason, programmers need to be especially careful when usingstd::unreachable(); get it wrong and there really are no compiler-provided safety nets, unlike some other forms of UB where there may be. And already it should be clear that the linked post is vastly oversimplifying things).Anything involving pointers is pretty prone to horrible consequences if there's UB - while a null pointer dereference typically does manifest in an immediate crash, things like buffer overrun and use-after-free can be much more subtle and much more insidious. This isn't news. It also applies mostly to a very limited subset of causes of UB. Throwing all UB in the same bucket is really misleading, at best. You'll occasionally see people (in reference to just how bad UB is) remark that even just having a missing newline at the end of a source file is UB. They always neglect to mention that it never, ever, actually causes the compiler to do anything significantly different than if the newline was present (beyond perhaps emitting a warning).
I also strongly suspect that the vast majority (I'm saying "vast majority", not all*) of actual runtime UB occurrences that don't involve dereference of invalid pointers are either harmless in effect or at least do exactly what you would expect (eg integer overflow typically wraps if that's what happens on the underlying architecture). The post is needlessly sensationalist, as was the preceding one.
*: notable exceptions include two classics, "the compiler ate my overflow check" and "I logged the value of an uninitialised variable but nothing happened" - even the latter one should cease to be an issue going forward with language changes that have been coming down the pipeline