I'm sorry, but what exactly is the problem with the code? I've been staring at it for quite a while now and still don't see what is counterintuitive about it.
A lot of compilers will optimize out a NULL pointer check because dereferencing a NULL pointer is UB.
Because assert will not run the following code in the case of a NULL pointer, AFAIK this exact code is still defined behavior, but if for some reason some code dereferenced the NULL pointer before, it would be optimized out - there are some corner cases that aren't obvious on the surface.
This kind of thing was always theoretically allowed, but really started to become insidious within the past 5-10 years. It's probably one of the more surprising UB things that bites people in the field.
GCC has a flag "-fno-delete-null-pointer-checks" to specifically turn off this behavior.
This is an actual Linux kernel exploit caused by this behavior where the compiler optimized out code that checked for a NULL pointer and returned an error.
Sure, but none of that is relevant to just the code snippet that was posted. The compiler can exploit UB in other code to do weird things, but that's just C being C. There's nothing unexpected in the snippet posted.
The issue is cause by C declaring that dereferencing a null pointer is UB. It's not really an issue with assertions.
You can get the same optimisation-removes-code for any UB.
> There's nothing unexpected in the snippet posted.
> The issue is cause by C declaring that dereferencing a null pointer is UB. It's not really an issue with assertions.
> You can get the same optimisation-removes-code for any UB.
I disagree - It’s a 4 line toy example but in a 30-40 line function these things are not always clear. The actual problem is if you compile with NDEBUG=1, the nullptr check is removed and the optimiser can (and will, currently) do unexpected things.
The printf sample above is a good example of the side effects.
> The actual problem is if you compile with NDEBUG=1
That is entirely expected by any C programmer. Sure they named things wrong - it should have been something like `assert` (always enabled) and `debug_assert` (controlled by NDEBUG), as Rust did. And I have actually done that in my C++ code before.
But I don't think the mere fact that assertions can be disabled was the issue that was being alluded to.
I wrote the comment, assertions being disabled was exactly what was being alluded to.
> that is entirely expected by any C programmer
That’s great. Every C programmer also knows to avoid all the footguns and nasties - yet we still have issues like this come up all the time. I’ve worked as a C++ programmer for 12 years and I’d say it’s probably 50/50 in practice how many people would spot that in a code review.
It's definitely a footgun, but the compiler isn't doing weird stuff because the assertions can be disabled. It's doing weird stuff because there's UB all over the place and it expects programmers to magically not make any mistakes. Completely orthogonal to this particular (fairly minor IMO) footgun.
> I’ve worked as a C++ programmer for 12 years and I’d say it’s probably 50/50 in practice how many people would spot that in a code review.
Spot what? There's absolutely nothing wrong with the code you posted.
Depends on where you're coming from, but some people would expect it to enforce that the pointer is non-null, then proceed. Which would actually give you a guaranteed crash in case it is null. But that's not what it does in C++, and I could see it not being entirely obvious.
If you don't even know what that would mean then it's premature to declare that nothing works that way. Understanding the meaning is a prerequisite for that.
In this case, it may help to understand that e.g. border control enforces a traveler's permission to cross the border, then lets them proceed.
Hard disagree. There are a lot of implementation "details" that, if you want to do properly, are a lot of hard work and very much nontrivial. For example, do try to write a compiler with efficient incremental compilation, and especially one that does so while also having optimization passes. And that's just one example, most things in compiler implementations actually turn out to be fairly complex. And lots of features that modern languages support e.g. more powerful typesystems, trait/typeclass systems, etc. are also very very tricky.
While designing a language is by no means trivial, it generally really occupies just a very small fraction of the language/compiler developer's time. And, in most cases, the two things (language design + implementation details) have to walk hand-in-hand, since small changes to the language design can vastly improve the implementation end.
To add to that, Rust code is generally not written to be 'exception-safe' when panics occur: if a third-party function causes a panic, or if your own code panics from within a callback, then memory may be leaked, and objects in use may end up in an incorrect or unusable state.
You really want to avoid sharing mutable objects across a catch_unwind() boundary, and also avoid using it on a regular basis. Aside from memory leaks, panicking runs the thread's panic hook, which by default prints a stacktrace. You can override the panic hook to be a no-op, but then you won't see anything for actual panics.
Yeah, I'm fairly sure that there is such a flag/toplevel attribute... and if there isn't, there should be one.
It also feels like most of the pains on avoiding panics centers around allocations which, though a bit unfortunate, makes sense; it was an intentional design choice to make allocations panic instead of return Results, because most users of the language would probably crash on allocation fails anyways and it would introduce a lot of clutter. There was some effort some while ago on having better fallible allocations, but I'm not sure what happened over there.
reply