7 ms·
I enjoy programming in C a lot, though I wish they would fix C's biggest mistake[1]. [1] <https://digitalmars.com/articles/C-biggest-mistake.html https://digit
by david2ndaccount 3y ago
I enjoy programming in C a lot, though I wish they would fix C's biggest mistake[1].
[1] <https://digitalmars.com/articles/C-biggest-mistake.html https://digitalmars.com/articles/C-biggest-mistake.html>
- jylam 3y agoC biggest "mistake" (there are historical reasons for this, though, but it doesn't make it acceptable nowadays) is the concept of undefined behavior, period.
- jovial_cavalier 3y agoEvery language has undefined behavior - it's just that some compilers emit an error when it's encountered. You can make a C compiler that refuses to compile code with undefined behavior.
- circuit10 3y agoThere is a tradeoff though because it allows the code to be optimised more which is often something you want from C code
- remexre 3y agoShould a compiler be allowed to optimize: int x = 0; int y = 42; foo(&x); bar(y); to: int x = 0; foo(&x); bar(42); without performing analysis on foo? If so, how can that optimization be legal without UB?
- anovikov 3y agoHow is this UB? foo() won't have any access to y so won't be able to modify it, so we can be more or less certain that y is still 42 when bar() is called? What is it that i'm missing?
- remexre 3y agoThat's it -- the optimization is legal because there's no way foo() could modify y, because any (&x)[1] = 20 shenanigans would be UB. This is how UB is used in optimizations; it's (usually) not that the compiler is finding positions where UB occurs and explicitly choosing to perform mischief, it's performing a bunch of simple and obviously-what-you-want rewrites that are only correct because of UB; otherwise, arbitrary pointer writes are too powerful.
- deleted 3y ago[deleted]
- Dylan16807 3y agoUB is what allows `(&x)[1]` to happen at all. If you didn't have UB, there would be no reasonable way to allow code that messes up arbitrary pointers. A typical non-UB language wouldn't allow `(&x)[1]` and it would be easy to allow the optimization. It would be difficult or obtuse to design a non-UB language that doesn't allow optimizing y away.
- remexre 3y agoStandard Forth, to my knowledge, doesn't have UB and consequently doesn't allow the optimization. That'd just be : foo CELL + 20 SWAP ! ; there. Machine languages also generally don't have this kind of UB, and don't allow the optimization. It's not impossible to imagine a C without UB, it's just not a particularly desirable language.
- Jtsummers 3y agoThe UB permits the optimization, but is not necessary for it. If a language had a stricter definition of pointer and memory access behavior then the optimization would still be applicable if it conformed to the assumption that people are making around the UB in C. UB is not necessary for optimization, it just permits some optimizations which may or may not be valid and may or may not be consistent across compilers and platforms because people get to fill in the gaps with whatever they want.
- torstenvl 3y agoI'm okay with undefined behavior, but the language should revert back to the original (an exhaustive list of permissible behavior by the host when the C abstract machine's behavior is undefined) rather than the current mere list of examples of possible behavior by the host.
- stouset 3y ago"I'm okay with undefined behavior, as long as its behavior is well-defined." You keep using that word. I don't think it means what you think it means. :)
- Dylan16807 3y agoA behavior like "corrupts whatever is in memory at that location" when your code screws up a pointer is not exactly "well-defined", but it does disallow "pretends it could have corrupted RAM and does whatever it feels like instead". To put that another way, you can treat certain behaviors as if they're opaque functions until after you're done optimizing. Similar to how you might handle volatile. Some of these changes would be very easy. For example, instead of treating "dereference zero" as unreachable code, treat it the same as "dereference unknown location".
- stouset 3y agoYour platform almost certainly does do something sane and reasonable for UB-exhibiting code. Where your problem lies is what happens when you optimize that code. And guaranteeing optimizations do something superficially sane on unsound code while still performing reasonable and obvious optimizations for well-formed code is insanely hard. The fundamental problem is that the design of C guarantees that lots and lots and lots of syntactically valid and intuitively written code is semantically total nonsense. Part of this is that C is low-level and part is just that it was designed 50 years ago before anyone had the faintest idea what kinds of terrible mistakes were being made. If there were simple fixes here it wouldn’t still be a problem.
- 3y ago
- pornel 3y agoOn the contrary, I don't think C would have been so ubiquitous without it. People use C for maximum speed and efficiency. When maximum speed isn't a requirement, there are plenty of more productive and less footgunny languages to choose from. Without the things that C allows to be ignored during optimization, you either wouldn't have compilers that optimize the "simple" language equally well, or you'd have to make the language bigger to give all the right hints and guarantees where they're needed. I see UB commonly characterized as compiler being mean for silly reasons, with implication that compilers could just stop doing the bad UB and do the good UB, but that isn't accurate or realistic view. The creeping UB is just a symptom of how optimization passes are implemented, and they would be exponentially harder to implement equally well without the concept of UB. "Just do what I mean" is not a well-defined compilation target, and there are tons of surprisingly bad performance issues. e.g. without signed overflow indexing of arrays by int could not use 64-bit CPU's addressing modes, because they overflow differently. You can use size_t, but good luck being diligent about it in a language that loves its ints.
- bsder 3y agoNo. The mistake is conflating "undefined behavior" with "unspecified behavior". Signed overflow was not intended to be undefined--it was unspecified because it has a perfectly acceptable specification that differs between processors, and it was expected that the compiler would define it. The fact that gcc and clang pick non-sensical specifications in order to gain 3% in performance was not something anybody expected back when this stuff was written. "Division by zero" is, on the other hand, genuinely undefined behavior. "Dereferencing a NULL pointer" is a genuinely undefined behavior. There generally isn't a good way for a program to continue after encountering them.
- aw1621107 3y ago> Signed overflow was not intended to be undefined--it was unspecified This seems wrong to me. The Standard very much calls signed overflow undefined behavior (Section 3.3): > If an exception occurs during the evaluation of an expression (that is, if the result is not mathematically defined or not representable), the behavior is undefined. In addition, overflow is explicitly listed as undefined behavior in at least two other places. For example, in section 1.6, Definitions of Terms: > An example of undefined behavior is the behavior on integer overflow. And in the list of undefined behavior (A.6.2): > An arithmetic operation is invalid (such as division or modulus by 0) or produces a result that cannot be represented in the space provided (such as overflow or underflow) (3.3). If signed overflow was not intended to be undefined, then it seems like a rather large oversight to say it's undefined behavior and to give it as an example of such. > because it has a perfectly acceptable specification that differs between processors, and it was expected that the compiler would define it. What you're describing is implementation-defined behavior in the C standard. If it was expected that implementations would define the behavior, why didn't the committee just... use that definition, instead of saying signed overflow is UB but actually meaning it's implementation-defined behavior? In addition, unspecified behavior is a third category of behavior distinct from undefined and implementation-defined behavior. The Standard imposes no requirements on unspecified behavior in a correct program, and does not require that an implementation document what it does in such a situation. [0]: https://port70.net/~nsz/c/c89/c89-draft.html https://port70.net/~nsz/c/c89/c89-draft.html
- tpoacher 3y agoUB is not a bug, it's a feature.
- neutrono 3y agoThe declaration syntax and the lack of a proper module/namespacing system is also a mistake, but perhaps not as big as the way arrays decays into pointers. Implicit conversions I suppose could also be classified as a mistake.
- JohnFen 3y agoWhat you're calling "mistakes" look to me like intentional design decisions made in order to optimize for specific kinds of problems. C is not a high level language, nor should it be. If you're doing the sort of work that is better suited for a high level language, then using a different language is the right call.
- neutrono 3y agoWell, IIRC even in the K&R book, a book which many C programmers admire for it's excellent documentation and prose, and rightly so, there is a specific section mentioning that C's declaration syntax has been castigated. Next, even if C is not a "high level language", which depend on your definition of "high level language", the perks of a proper module system for low level coding cannot go unmentioned. - Rust, a language that lives in a somewhat similar abstraction and power space as C, has a module system. - C++, a language that lives closer to C in features, also now, in C++23 I believe, has a module system, and before that, had namespaces. On implicit conversions, I think having implicit conversions are hard to get right, and I think that C has them is not a feature, but rather a bug. Languages that have taken some inspirations from C, has chosen usually to do away with implicit conversions, or at the very least, limit their use. And the array -> pointer in functions, I percieve to be a big mistake, not at the time, perhaps it was not known better ways, but as the years roll by, I think it's a misfeature given the countless exploits that have taken place due to a read past the bounds of an array. I think C is a good language, but these misfeatures burdens the language in an undue way, and I think a better way to design a language is to be more explicit. Something like Rust's unsafe { /* Here be dragons */ } is better design, in my opinion. I think if you want implicit conversions, there should be an explicit way to declare that you want things to be implicit. That's just my 2¢
- dang 3y agoPast discussions on that: C's Biggest Mistake - https://news.ycombinator.com/item?id=27567311 https://news.ycombinator.com/item?id=27567311 - June 2021 (8 comments) C’s Biggest Mistake (2009) - https://news.ycombinator.com/item?id=24454369 https://news.ycombinator.com/item?id=24454369 - Sept 2020 (374 comments) Conflating pointers with arrays: C's biggest mistake? (2009) - https://news.ycombinator.com/item?id=17585357 https://news.ycombinator.com/item?id=17585357 - July 2018 (254 comments) Walter Bright on C's Biggest Mistake - https://news.ycombinator.com/item?id=1014533 https://news.ycombinator.com/item?id=1014533 - Dec 2009 (47 comments)
- tpoacher 3y agoI don't entirely agree with this article ... it's quite possible to "pass" an array as a pointer to an array type, as opposed to a decayed pointer, e.g. void f( int (*a) [5] ); int a[5] = {1, 2, 3, 4, 5}; f( &a ); The larger problem here is more that this mechanism doesn't support arbitrary sizes. Which probably makes sense from how c handles memory, but it's very restrictive. Incidentally, I recently had an argument with a friend who had been "coding for years" in c, that the memory address of "a" and "&a" in the example above are identical. He thought it was so obvious I was wrong (because pointers) that I had to code it and print the addresses for him and prove they're the same. He was completely mind-blown xD
- david2ndaccount 3y agoYou can’t pass an array by value and you can’t pass a pointer with a corresponding length of non-fixed size (you can actually with VLAs, but frankly the syntax sucks and not all compilers support it). This missing language feature has led to horrible things like nil-terminated strings , a million bespoke ways to encode the length of the array, or just functions that pray you have enough space like `strcpy` and `gets`.
- tpoacher 3y ago> You can't pass an array by value Sure, but, this is the case in all languages, no? Otherwise, what should be the convention? A deep copy? A first-degree shallow copy? You simply can't avoid the referential nature of arrays for use as arguments to functions. So the issue here shouldn't be the referential nature of this, but the fact that they chose to implement it as a "decaying" pointer as opposed to a "pointer to array type of a certain size". And hence why I'm pointing out there is, in fact, a (relatively simple) way to do this, but only for fixed sizes. But the reason it's not THAT useful in practice is because, in general, you probably wouldn't want the function's parameter to expect a fixed size array, but you'd probably want the flexibility of a variable-sized array, whose length is defined at runtime. And this is what is not supported by c. So in some sense, in the absence of support for variable-sized array parameters, then not expecting fixed array sizes at the point of defining function parameters, and using a decaying pointer instead (which simply doesn't care about the size), is probably the lesser evil of the two. So for me the 'bigger' evil isn't decaying pointers, but the lack of support for VLAs as function parameters. This would have allowed you to use the above syntax to pass a pointer to an array type, retaining full information about the pointed array, including its size. (in other words I think we're saying a very similar thing but with different words)