I don't agree with that description at all. Here's the code:
1 if (argc <= 2)
2 unreachable();
3 else
4 return printf("%s: we see %s", argv[0], argv[1]);
5 return puts("this should never be reached");
The only code path that's "entirely different" is lines 1,4,5 and in that case of course you remove a return that's after a return.And the other valid code path is 1,2,5, which has `puts` after `unreachable`.
To need `puts` you have to imagine a code path that gets past the "if" without taking either branch?
Maybe the author means something by "code path" that's very different from how I interpret it?
I would be pretty surprised if the above code means something different from:
if (argc <= 2) {
unreachable();
return puts("this should never be reached");
} else {
return printf("%s: we see %s", argv[0], argv[1]);
return puts("this should never be reached");
}If someone really just wants a delay, it's easy to either (for programs running on normal OSs) call a sleep function, or (on tiny embedded systems) add an empty inline assembler statement that the compiler can't see through.
I've been many loops that turn into no-ops because all the functionality has been refactored out but this fact is hidden in function calls.
Sure, this should ideally be surfaced as a lint error, not a compiler optimization, but you cannot say that intentional delays are the "only" reason.
Also since processing time is variable, using that as a method should be extremely heavily discouraged/warned/require-opt-in
It would be of course nice if a warning was produced for that specific case: This whole loop was removed - is it really what you wanted, or is it a broken delay loop?
return printf("%s: we see %s", argv[0], argv[1]);
IOW, the conditional has been elided. But you're right in that the wording of the complaint doesn't match the example. The author presumably had in mind some of the more infamous NULL pointer-related optimizations, without spending the time to put together a properly analogous example. 1 if (argc <= 2)
2 puts("A");
3 puts("B");
4 if (argc <= 2)
5 unreachable();
6 else
7 return puts("C");
8 return puts("D");
in which not just lines 4-6,8 go away (as you said) but also lines 1-2.It makes sense to me but I can see why the author would characterize this situation as "license to use an unreachable annotation on one code path to justify removing an entirely different code path that is not marked unreachable". In a different world one might expect A to be printed "before the UB happens".
if (argc <= 2)
do_something();
else
return printf("%s: we see %s", argv[0], argv[1]);
So the `return printf` is executed when `argc` is greater than 2. If we remove just the body of the first branch: if (argc <= 2)
;
else
return printf("%s: we see %s", argv[0], argv[1]);
the same thing holds. And additionally when `argc <= 2`, control will move past the `if`.Under this view, if the `unreachable` won't cause the entire removal of the `if`, the compiler will produce the equivalent of:
if (argc > 2)
return printf("%s: we see %s", argv[0], argv[1]);
return puts("this should never be reached")
Again, I don't say this is the correct interpretation, but it is one possibility, that would have to be ruled out by other parts of the standard.The `realloc()` change though...
The predominant focus is realloc(pre,0) becoming UB instead of what the author misleadingly describes as useful, consistent behaviour. It is far from that, and that’s the entire reason that it was declared UB in the first place: https://www.open-std.org/jtc1/sc22/wg14/www/docs/n2464.pdf. Note that this wasn’t a proposal to change something, it’s a defect report: the original wording was never suitable.
The second part is the misconception about the impact of UB. Making something UB does not dictate that its usage will initiate the rise of zombie velociraptors. It grants the implementation the power to decide the best course of action. That is, after all, what they’ve been doing all this time anyway.
Note that this deviates from implementation-defined behaviour, because an implementation-defined behaviour has to be consistent. Where implementations choose to let realloc(ptr,0) summon the zombie raptors, they are free to do so. Don’t like it? Don’t target their implementation. Again, this isn’t a change from the POV of implementers - it’s a defect in the existing wording.
In this case, the course of action that any implementation will choose is to stick with the status quo. It is clearly not a deciding factor in whether or not you embrace the new standard, and to suggest otherwise is dishonest, sensationalist nonsense. The feature was broken, and it’s just being named as such.
That being said, you're completely wrong about what UB means. Making use of UB may as well initiate the rise of zombie velociraptors. Except for the situation where your implementation explicitly specifies that it provides a predictable behaviour for a specific case of UB, there's literally no guarantee of what will happen. Assuming that the implementation will stick with some status quo and your code won't exhibit absolutely unusual behaviour is just naiive.
Please don't mislead people into thinking that it's ever a good idea to assume that undefined behaviour will be handled sensibly, this kind of mislead assumption is one of the major sources of bugs in C code.
This is not even close to be true. Most bugs in C code are from programmer mistakes, not from UB behavior. The exaggeration that is spread by some people regarding UB is close to absurd. If something is UB, it may generate different results in different situations, even with the same compiler. The standard is just clarifying this problem. A good compiler will do something sensible, or at least issue a warning when this situation is detected. If you have a bad compiler that does strange things with your code, it's not a defect of UB but the compiler instead.
That situation is "when you have UBSan turned on".
Wrong, Wrong, Wrong.
UB allows the implementation to take any arbitrary course of action, without informing anyone, without documentation, without any conscious decision, without weighing anything to be better/worse. Nondeterministically catching fire and launching nuclear rockets is a completely compliant reaction to UB.
What you are describing is "implementation defined" behavior. That has to be deterministic, documented, and conforming to some definition of sanity. Examples are the binary representation of NULL, sizes of integer types or stuff like the maximum filename length. Sadly, too many things in C have "undefined behavior", too few have "implementation defined" behavior.
And UB has always been an excuse for compilers to screw over programmers in hideous ways. Programmers are rightfully afraid of any kind of new UB being introduced, because it will mean that whole new classes of bugs will arise because the compiler optimized out that realloc(..., a) where a might be 0, because thats UB, so screw you and your code... And this change is especially dangerous because it makes a lot of existing code UB.
Your reply was great up until this. Compiler writers aren’t looking to screw over programmers, they’re looking to make code faster. UB gives them the ability to make assumptions about what is and is not true, at a particular moment in time, in order to skip doing unnecessary work at runtime.
By assuming that code is always on the happy path, you can cut a lot of corners and skip checks that would otherwise greatly slow down the code. Furthermore, these benefits can cascade into more and more optimizations. Sometimes you can have these large, complicated functions and call graphs get optimized down to a handful of inlined instructions. Sometimes the speedup can be so dramatic that the entire application is unusable without it!
Many of these optimizations would be impossible if compilers were forced to assume the opposite: that UB will occur whenever possible.
The tool programmers have available to them is compiler flags. You can use flags to turn off these assumptions, at the cost of losing out on optimizations, if your code needs it and you’re unable to fix it. But it’s better to turn on all possible warnings and treat warnings as errors, rather than ignoring them, to push yourself to fix the code.
Since approximately every nontrivial program ever written has UB, in actual practice we're only saved by the fact that compilers aren't entirely maliciously compliant.
This isn't a case of compilers screwing over the programmers, because the people who are responsible for those optimizations are the people who are scratching their heads as to why it's UB and not impl-defined behavior.
int n;
printf("type 0 to stop the rise of zombie velociraptors");
scanf("%d", &n);
realloc(pre, n);
if (n != 0) rise_zombie_velociraptors()
May result in velociraptors raising even if the user enters "0".The reason is that because realloc(pre, 0) is UB, for the compiler, it cannot happen, so n can't be 0, so the n != 0 test can be optimized out, so, velociraptors.
Wrong. UB never happens. That is the promise the program writer makes to the compiler. UB never happens. A correct C program never executes UB. This allows the compiler to assume that anything that is UB never happens. Does some branch of your program unconditionally execute realloc(..., 0) after constant propagation? That branch never happens and can just be deleted.
Reading the defect report, they state "Classifying a call to realloc with a size of 0 as undefined behavior would allow POSIX to define the otherwise undefined behavior however they please." which is wrong. UB cannot be defined, if you define it, you are no longer writing standard C. It should instead have been classified as "implementation-defined behaviour".
In any case it's not that hard to just write a sane wrapper. This one is placed in the Public Domain:
void *sane_realloc(void *ptr, size_t sz)
{
if (sz == 0) {
free(ptr); /*free(NULL) is no-op*/
return NULL;
}
if (ptr == NULL) {
return malloc(sz);
}
return realloc(ptr, sz);
}
I am calling it sane and not safe, because it is not safe. You still have the confusion of what happens when the function returns NULL (was it allocation failure or did we free the object?) - check errno. However, it has the same fully defined semantics on most all implementations and acts like people would expect.You may be tempted to make the function return the value of errno, mark it [[nodiscard]] and take a pointer-to-pointer-to-void, so that the value of the pointer will only be changed if the reallocation was successful. I am not sure if that is safer. You are trading one possible bug - null pointer on allocation failure, which then will cause a segmentation fault for another - stale pointer on allocation failure, but with updated size. The latter is more likely to be used in buffer overflow attacks than the former.
The first sight of "catch fire" might not have caught my attention, but by the time it got to "instrument of arson" and "Molotov cocktails", the style was sufficiently distracting that I was convinced I wasn't the intended audience.
So the feature wasn't broken to begin with, it was broken by another feature.
It does nothing trickier than any other kind of UB. In fact, I could implement unreachable() like this: void unreachable() { (char *)0 = 1; }.
Standardizing it however gives interesting options for compilers and tool writers. The best use I can find is to bound the values of the argument of a function. For example, if we have "void foo(int a) { if (a <= 0) unreachable(); }, it tells the compiler that a will always be >0 and it will optimize accordingly, but it can also be used in debug builds to trigger a crash, and static analyzers can use that to issue warnings if, for example, we call foo(0). The advantage of using unreachable() instead of any other UB is that the intention is clear.
For example:
assert(a >= 0);
if (a < 0) printf("a is negative");
In release mode, assert() will be gone, so the if/printf() will stay. If we used "if (a < 0) unreachable();" instead of assert(), it would optimize away both lines.If you still want to use C, for example for compatibility reasons and want to make it safer, assert isn't going away (unless you set NDEBUG). Preconditions are not "inevitably violated", there are ways of making sure they aren't, and I think an explicit "unreachable()" can help tools that are designed for that purpose.
Should you profile first before using unreachable() for optimization purposes? Maybe, but the important part is that now, you have a way of clearly and effectively tell the compiler what you know will never happen so that it can optimize accordingly, whether it is before or after profiling.
Compilers usually do a great job at optimization, but there are often some edge cases the compiler have to take into account in order to generate code that complies with the C standard, and it can have an impact on performance. unreachable() is one way to tell the compiler "please forget about the edge case, I know it won't happen anyways", the best part is that it is explicit, no obscure tricks here.
Side note about profilers: no matter what your strategy is with regards to optimization, I think profilers are essential tools that don't get enough attention. People talk a lot about linting, coverage and unit tests, but profilers are not to be left out. They are not just tools that tell you where not to optimize your code, they can also find bugs, mostly performance bugs, but not only.
How can it be used to trigger a crash (a specific behavior) if the behavior it invokes is undefined? Are you saying it would be defined differently for debug builds so that it doesn't invoke undefined behavior?
All I can do is laugh. This is what the dynamic linker fanatics wanted. This is what they explicitly advocate for to this day. Share and enjoy!!
However, the author is unlikely to be correct here. E.g., to this day, glibc contains _multiple implementations of memcpy_ just to satisfy those executables that depend on the older, memmove-like behavior that was once part of the unspecified behavior of glibc. The only way to get the dynamic linker to choose one of the newer versions is to, well, rebuild the executable. It is inconceivable that glibc would not use symbol versioning with an actual specification change.
The behavior is practically the same as with static linking, and you still get the benefits of dynamic linking.
If I have something that critical, I can always statically compile.
As a french guy I'd go with (d).
I've often seen "toto" used as a placeholder name, sometimes followed by "titi", "tata", "tutu", I have even used it myself. It is similar to "foo", "bar", "baz". I don't know if it is specific to France, of French speaking countries, but it is definitely a thing here.
Jens Gustedt is part of the C comity and participated to C23. He also works for INRIA in France: https://en.wikipedia.org/wiki/French_Institute_for_Research_...
Not because these functions couldn't handle it, but because this assertion simplifies optimizations elsewhere.
This has required adding extra checks in my code, found mainly by trial and error, and has made it less readable and less optimal.
Finally, the checked arithmetic operations returning false on success is a horror show. Fortunately it will be found on the first time the code is run, but that's a damnably low bar :(
This seems in line with C conventions? Generally a 0 return code means success.
“If checked operation has a status, then it failed.” - ok
“If checked operation [is true], then it failed.” - wat
That's what got you? C functions returning error flags (with zero meaning no error) isn't exactly new.
C inventor Dennis Ritchie pointed to several flaws in [ANSI C] ... which he said is a licence for the compiler to undertake agressive opimisations that are completely legal by the committee's rules, but make hash of apparently safe programs; the confused attempt to improve optimisation ... spoils the language.
—Dennis Ritchie on the first C standard
A 7 letter function to add two numbers and that returns a boolean... not entirely sure I'd call that 'sane'.
I wrote a portability library that wraps these with compiler intrinsic and standard C fallbacks. I chose to spell out the full word in addition to making the type explicit. It's a lot more verbose of course but a lot clearer to read:
https://github.com/ludocode/ghost/blob/develop/include/ghost...
"a + b = c;" is a fundamentally flawed operation from a computer architecture perspective.
The other way isn't really definable as an assignment mathematically.
And there is a lot more to it than just pass/fail. First, an addition doesn't fail, from a computer architecture perspective, the addition will always succeed, the only thing that could fail (in all the usual architectures) are possible memory fetch and store operations when not strictly dealing in register or immediate operands. Second, there is no fail flag. There is a overflow flag, an underflow flag, a zero flag, a sign and a few more that are irrelevant here. Any of overflow, underflow, zero or sign might mean that the operation "failed" depending on the types of your operand. Where the processor doesn't know anything about the type, so there won't be a straightforward 'fail' flag in any case. Only the library or compiler can use type information such as (un)signedness, bignum-ness, nonzeroness, desired wraparound (for modular types) and other possible types together with aforementioned flags to decide if that addition might have failed.
So nothing is fundamentally flawed, what you are describing is just insufficiently complex (because there is no fail flag, just a ton of other flags) or overly complex (because uint32_t c = a + b is modular 2^32 arithmetics and cannot fail).
> The other way isn't really definable as an assignment mathematically.
This correction is condescending and unnecessary. Unless the person had never written a single line of code in their life, then they would obviously know "a+b" is not a modifiable lvalue.
And the point about pass/fail was also obviously not mean to capture the full complexity of the flags set by a CPU operation. It was very clearly a statement about how basic addition does not behave in computers the way it does on paper -- as simple as that.
From HN guidelines: "Please respond to the strongest plausible interpretation of what someone says, not a weaker one that's easier to criticize."
It's an equality sign. See also, := and unification.
A more sophisticated type system.
Let's say you had some pseudocode like this:
let a = 5
let b = 12
let c = a + b
The type of a would be Integer[5..5], the type of b would be Integer[12..12], the type of c would therefore be Integer[17..17]. In a more complex example: def foo(a: Integer[0..10], b: Integer[0..10]):
return a + b
The return type of this function would be Integer[0..20].This kind of type system can solve a number of issues, all but division by zero (which would probably still have to be solved with some kind of optional type).
If type inference dictates that the upper range of an integer would be too large to physically store in a machine data type, then you either resort to bignums or you make it a compilation error. By adding modular and saturating integer types you can handle situations where you want special integer behaviours. By explicitly casting (with the operation returning an optional) you can handle situations where you want to bound the range. This drastically simplifies a lot of code by removing explicit bounds checks in all places except where they are absolutely necessary. If for some reason you care about the space or computational efficiency of the underlying machine type, you can have additional annotations (like C's u?int_(least|fast)[0-9]+_t). If you absolutely must map to a machine type (this is usually misguided, unless you are dealing with existing C interfaces, for which such a language can provide special types) you can have more annotations.
Ada has something resembling this. I believe there are some other languages that implement similar features. I believe this sort of thing has a name, but I am not great with remembering the names of things.
Hopefully this is some food for thought.
https://en.wikipedia.org/wiki/Refinement_type
But the concept is just a little bit over 30 years old. So don't expect it shows up in most mainstream languages before the end of the next 20 years, and don't expect it to come to the C languages ever.
Meanwhile in mainstream ML-land:
https://github.com/Iltotore/iron
(Or for the older version of the language: https://github.com/fthomas/refined)
(Please also note that for this feature both versions don't need language support at all but are "just" libraries, as the language is powerful enough to express all kinds of type level / compile time computations in general.)
[status, value] = add(a, b);
Is much more unparalleled-ly (?) readable from the perspective of how a computer actually operates. In reality, this:
uint c = (uint)a + (uint)b; // (to make that other guy happy)
is really:
c = (a + b) % (sizeof(uint));
in "C", which is less readable but far more accurate.
It's a funny thing to say.
nervous Minkowski laughter
I learnt C back when K&R (first edition) was the reference. Ok, it was hardly much more than a universal assembler to make every computer look like a PDP-11. In my experience C is the language to use when you want to be close to the metal. For the rest I use which ever high-level language/environment is best suited. Admittedly some FFI are a pain to use, but once you get the boilerplate bedded down your much higher level language gets the coordination done.
Isn't that what standards are supposed to do?
The alternative approach is to invent things by committee, hopefully with some implementers watching, and hope for the best.
I don't agree with this in the slightest. I'm not "outraged" by undefined behaviour, it's a fundamental tool for writing performant code. Ensuring that dereferencing a null pointer or accessing outside the bounds of an array is undefined behaviour is what lets the compiler not emit a branch on every array access and pointer dereference.
Furthermore, I really don't understand the outrage that there is another explicit tool to achieve behaviour the author may or may not consider harmful. If it's an explicit macro, it's not a tarpit!
(don't get me wrong. love C. but in an innocent sort of way, like a teenager quite unaware of betrayals, heartbreak, love triangles, or UB, UsB, and IDB..)
What's the reason for this?
void *p = malloc(N);
do_random_stuff(p);
void *q = malloc(N);
With this rule, the compiler can conclude that p and q cannot alias, even if it doesn't have body of do_random_stuff. Without it, it would first have to prove that p is never freed before calling q, which is basically impossible (moving the body of intervening code into a different file, for example, would do the trick).I can imagine situations where a pointer q might sometimes be a copy of pointer p and sometimes might point to something else, and the code wants to free q if and only if it is not a copy of p (because p has been free'd earlier).
Seems a bit tone deaf to create new undefined behavior in memory handling, especially when a sane default behavior seems to be de facto
I've used that free-on-0 behavior myself. Unfortunately the code that uses this will often have 0 be a length variable, so hard to grep for this. Ideally musl/glibc will both stick to that undefined behavior being free & gcc/clang won't go about making this something to point their optimizations at
Lest we have to stop using realloc outside of a safe_realloc wrapper
static void *safe_realloc(void *p, size_t newlen)
{
if (newlen == 0) { free(p); return NULL; }
return realloc(p, newlen);
}
What got this whole thing weird is that C doesn't like zero sized objects, but implementations were allowed to return a unique pointer for a zero sized allocation. Which then raises the matter that being portable there require freeing that reserved chunk for non-free implementations. In theory this reservation code could be more efficient when code frequently reallocates between 0 & some small value. & there was uncertainty because NULL is a way to say allocation failure, but then if one did a NULL check on realloc's return value they also had to check that the size was non-zeroIt's only tone deaf to people who understand "undefined behavior" as an epithet or as synonymous with giving a license to compilers to screw you over. The term doesn't have either of those meaning to those on the C committee. In fact, one of the explicit rationales for the proposal is that, "Classifying a call to realloc with a size of 0 as undefined behavior would allow POSIX to define the otherwise undefined behavior however they please." https://www.open-std.org/jtc1/sc22/wg14/www/docs/n2464.pdf
> especially when a sane default behavior seems to be de facto
The above proposal, N2464, gives the behavior for AIX, zOS, BSD (unspecified), MSVC (crt unspecified), and glibc. They each have different behaviors.
Why they chose to finally make it undefined (it was marked as obsolescent for a long time) rather than keep it as implementation-defined, I don't know. Perhaps because it 1) simplifies the standard, and 2) by making it undefined it suggests compilers should start warning about it--despite all this time neither has there arisen a consensus among implementations about the best behavior, nor are programmers aware that the behavior actually varies widely.
EDIT: The draft SUSv5/POSIX-202x standard has indeed directly addressed this issue. See, e.g., https://www.austingroupbugs.net/view.php?id=374 The most recent draft included the following addition to RETURN VALUE:
OB If size is 0,
OB CX or either nelem or elsize is 0,
OB either:
OB * A null pointer shall be returned
OB CX and, if ptr is not a null pointer, errno shall be set to [EINVAL].
OB * A pointer to the allocated space shall be returned, and the memory object pointed to by ptr
shall be freed. The application shall ensure that the pointer is not used to access an object.
CX marks points of divergence with C17. The first CX is because of the addition of reallocarray, absent from C17. The second is because POSIX will mandate the setting of EINVAL if NULL is returned.It's unfortunate but not surprising that the C committee isn't aware of the problems with the undefined behavior.
In fact, after I started reading WG14 meetings minutes, I completely lost faith that any of the serious problems with the standard will ever get fixed.
Unfortunately, this is the correct understanding of UB.
Some of the windows API's work like this, so how much is pressure from MS?
Same discussion from 7 months ago.
https://news.ycombinator.com/item?id=32352965
https://thephd.dev/c23-is-coming-here-is-what-is-on-the-menu...
https://www.open-std.org/jtc1/sc22/wg14/www/docs/n2897.htm
Pattern matching ram for variables/objects whilst they exist even if zero'ed or prefilled with a value doesnt give perfect security. Random values would make it harder to work out the variable/object.
> Standard C advances slowly
They're not joking, either. C is conservative to a fault, I think.
If you want to try out those features now, I made a pre-processor that translates that into standard C99:
https://sentido-labs.com/en/library/cedro/202106171400/use-e...
https://sentido-labs.com/en/library/cedro/202106171400/#numb...
It includes a cc wrapper called cedrocc that you can use as a drop-in replacement:
https://sentido-labs.com/en/library/cedro/202106171400/#cedr...
The security world will keep burning it seems.
There is no alternative to network protocols and IPC that the stringtypes C has. You get a length and a byte array. If you trust the user, you can assume length is correct. Otherwise no.
In fact Ethernet early days goes back to Mesa not C.
UNIX did not invent networking, networking predates UNIX for at least a decade.
For bonus marks, int and atomic_int are unrelated types, and simd vector types aren't a thing, so enjoy the unfixable performance cost of choosing C.
sob
But this will speed the transition to Rust.
I'm surprised that the authors decided to, and were able to, slip in this little euphemism.
I haven’t seen widespread use of the word “neurodivergent” as a kind of… whatever this is, weirdly euphemistic slur, almost?
It’s a continuation of the euphemism treadmill [1]. It won’t be long before “neurodivergent” is considered politically incorrect and a new term is invented to replace it.
[1] https://www.urbandictionary.com/define.php?term=Euphemism%20...
And yet again, these Lincoln systems mess up. While giving advice to the author can avoid Great Grimsby mistakes, making the replacements automatically is an utterly Scunthorpe decision, with failures as Slough as they are foreseeable.
If the intent is to be unhurtful, it should need to choose a different word to if the intent is to be hurtful! Even our most sophisticated automated systems are Milton Keynes at determining that kind of thing.
It does require some abstract thinking to comprehend sets of zero measure, negative measure or complex measure in mathematics. A "zero length object" is also encountered pretty often in practice:http://docs.autodesk.com/CIV3D/2013/ENU/index.html?url=files... and zero-length files come to mind.
The euphemism ends up working out fine, though likely not the author's intent.