6 ms·
And presuming you avoid 100% of undefined behavior, which I've never seen a non-trivial C program succeed at. C is way too complicated in the real world. You do
by chowells 2y ago
And presuming you avoid 100% of undefined behavior, which I've never seen a non-trivial C program succeed at. C is way too complicated in the real world. You don't want C, you want a language that actually gives defined semantics to all combinations of language constructs.
- einpoklum 2y agoYou don't necessarily want that. Forcing language-defined semantics on everything costs performance. Sorry, it just does, we can't have it all. So, you can sacrifice performance for well-defined'ness, or you can choose not to - and the choice depends on the language _design goals_. As the design goals differ, so do the combinations of choices made for syntax and semantics.
- bigstrat2003 2y agoI think pretty much any amount of performance is worth sacrificing in order to get rid of the gnarly things UB can cause. Correctness is the first and most important thing in programming, because if you can't be certain it works then it's not very useful.
- einpoklum 2y agoIt may be worth it _for you_. It is not worth it _for others_. Correctness can be established well enough - even if guaranteed automatically - in a language with UB.
- fooker 2y ago>you want a language that actually gives defined semantics to all combinations of language constructs No, this is wrong. It's a common misconception though. You would only want that in a hypothetical world where all computers are exactly the same. Undefined and implementation defined behavior is what allows us to have performance at all. Here are some simple examples. Suppose we want to make division by zero and null pointer dereference defined. Now every time you write a/b or *x, the compiler will be forced to emit an extra branching check before this operation. Something much more common---addition. What about signed overflow? Do you want the compiler to emit an overflow check in advance? Similar reasoning for shift instructions. UB in the language specification allows compilers to optimize based on the assumption that the programs you write won't have undefined behavior. If compilers are not able to do this, it becomes impossible to implement most optimizations we rely on. It's a very core feature of modern language specifications, not an oversight you can fix by thinking about it for 10 minutes.
- grumpyprole 2y agoI'm sorry I still don't buy it. Can you please show me a use case where ignoring null pointer or overflow checks makes your product non-viable or uncompetitive? Some of these checks could be removed by languages with better compilers and likely more restrictions. That is the better approach. As a user, I don't want to run code that is potentially unsafe and/or insecure.
- fooker 2y agoIt's not ignoring about ignoring null pointer or overflow checks, it's having to necessarily insert these checks everywhere.
- grumpyprole 2y agoWe should build compilers that insert these checks for us (if they cannot statically determine them unnecessary). The ability to omit these checks doesn't IMHO justify undefined behaviour.
- fooker 2y agoWell, good news is that you have optional modes in most compilers that do this. You would not want to force these by default, nobody wants it. You can not statically determine them unnecessary in for the vast majority of code, even stuff as simple as `print(read(a) + read(b))`.
- daemin 2y agoSo the simplest case for not providing a language specification for dereferencing a null pointer is that it requires putting in checks everywhere to detect the condition and then do something in the case where the pointer is null. So what should the null pointer case do then? Something like emit an exception, or send a signal, or call std::terminate to exit the process? I know that languages like Java have a NullPointerException which they can throw and handle for situations like this, but they're also built on a highly specified virtual machine architecture that is consistent across hardware platforms. This also does not guarantee that your program is safe from crashing when this exception gets thrown, as you have to handle it somewhere. For something as general as this it will probably be in the Main function, so you might as well let it go unhandled as there's not that much you can do at that point. For a language like C++ it is simpler, easier, and I would argue more correct, to just let the hardware handle the situation, which in this case would trigger a memory error of trying to access invalid memory. As the real issue is probably somewhere else in the code which isn't being handled correctly and the bad data is flowing through to the place where it accesses the null pointer and the program crashes. To add to that in a lot of cases the program isn't crashing while trying to access address 0, it's crashing trying to access address 200, or 1000, or something like that, and putting in simplistic checks isn't going to catch those. You could argue that the check should guard against accessing the lowest 1k of memory, but then when do you stop, at 64k? Then you have an issue with programs that must fit within 1k of memory. Leaving it unspecified is the better choice.
- kimixa 2y agoI feel the meme of "Undefined Behavior" has been massively exaggerated on the internet - the vast majority of examples appear to be extreme toy examples using the weirdest contrived constructs, or things that are expected to fault and you're already using platform-specific information to know what that would look like (e.g. expecting a segmentation fault). It's a Scary Boogyman That Will Kill You, not something that can be understood, managed, and avoided if necessary. And even then there are tools to help define much of that - if you want well defined wrapped signed integers, great. If you want to trap on overflow, there's an option for that. Lots of compiler warnings and other static analysis tools (that would just be default-rejected by the compiler today if it didn't have historical baggage, but they exist and can be enabled to do that rejection). Yes, there's many issues with the ecosystem (and tooling - those options above should be default IMHO), but massively overstating them won't actually help anyone make better software. And other languages often have similar amounts of "undefined behavior" - but just don't document it as such, relying on a single implementation being "Defined Correct", and hope they're not actually being relied on if anything changes. Just like C, only undocumentated.
- adrianN 2y agoI don't feel like the cause of most (all?) memory safety bugs has been "massively exaggerated".
- kimixa 2y agoIf you removed every case of "Undefined Behavior" from the C spec, you'd still have memory safety bugs. Because they're orthogonal (though may be coupled if they come from the same core logic error). This is what I mean by it becoming "meme" - things like "Undefined Behavior" or "Memory Safety" have become a discussion-ending "Objective Badness", hiding the real intent - being "Languages I Do No Like" (or, most often, are a poor fit for the actual job I'm trying to do. Which is fine, but not rejecting that those jobs actually exist). But they mean real things that we can improve in terms of software quality, and safety - but that's rarely the intended result when those terms are now brought up. And many things we can do right now with existing systems to improve things, to not throw away huge amounts of already well-tested code. To do a staged improvement, and not let "perfect" be the enemy of better.
- throwaway2037 2y ago> You don't want C, you want a language that actually gives defined semantics to all combinations of language constructs. So, Zig?
- sixfiveotwo 2y agoWell, perhaps a subset of it, since it also introduces concepts that do not exist in C (eg. exceptions).
- samatman 2y agoZig does not have exceptions, what it has is error sets. It uses the words try and catch, which does cause confusion, but the semantics and implementation are completely different. If a function has an error type (indicated by a ! in the return type), you have a few options. You can use `result = try foo();`, which will propagate the error out of the function (which now must have ! in its signature). Or you can use `result = foo() catch default;` or `result = foo() catch unreachable;`. The former substitutes a default value, the latter is undefined behavior if there's an error (panic, in debug and ReleaseSafe modes). Or, just `result = foo();` gives `result` an error-union type, of the intended result or the error. To do anything useful with that you have to unwrap it with an if statement. It's a different, simpler mechanism, with much less impact on performance, and (my opinion) more likely to end up with correct code. If you want to propagate errors the way exceptions do, every function call needs a `try` and every return value needs a ! in the return type. Sometimes that's what you need, but normally error propagation is shallow, and ends at the first call which can plausibly do anything about the error.
- sixfiveotwo 2y agoThank you for your input, I stand corrected. So as I understand it, it works somewhat like the result type of rust (or ocaml), or the haskell either type, but instead of being parameterized, it is extensible, isn't it?
- samatman 2y ago
- planede 2y agoHow do you define a buffer overflow?