7 ms·
The problem is that c and c++ have a ridiculous amount of undefined behavior, and it is extremely difficult to avoid all of it. One of the advantages of rust i
by thayne 2y ago
The problem is that c and c++ have a ridiculous amount of undefined behavior, and it is extremely difficult to avoid all of it.
One of the advantages of rust is it confines any potential UB to unsafe blocks. But even in rust, which has defined behavior in a lot of places that are UB in c, if you venture into unsafe code, it is remarkable easy to accidentally run into subtle UB issues.
- layer8 2y agoIt’s true that UB is not intuitive at first, but “ridiculous amount” and “difficult to avoid” is overstating it. You have to have a proof-writing mindset when coding, but you do get sensitized to the pitfalls once you read up on what the language constructs actually guarantee (and don’t guarantee), and it’s not that much more difficult than, say, avoiding panics in Rust.
- pjmlp 2y agoSo surely you know by hear the circa 200 use cases documented in ISO C, and the even greater list documented in ISO C++ standard documents. Because, me despite knowing both since the 1990's, I rather leave that to static analysis tools.
- tomjakubowski 2y agoIn my experience it is very easy to accidentally introduce iterator invalidation: it starts with calling a callback while iterating, add some layers of indirection, and eventually somebody will add some innocent looking code deep down the call stack which ends up mutating the collection while it's being iterated.
- layer8 2y agoI can tell you that this happens in Java as well, which doesn’t have undefined behavior. That’s just the nature of mutable state in combination with algorithms that only work while the state remains unmodified.
- School-Cotton 2y agoOkay, but that’s the point. This is UB in C/C++, but not in Java, illustrating the fact that C and C++ have an unusually large amount of UB compared to other languages.
- usefulcat 2y agoI'm familiar with that very sort of bug, but I don't see how it's a failure of the language. To be convinced of that I think I'd at least need to be shown what a good solution to that problem would look like (at the level of the language and/or standard library).
- demurgos 2y agoRust statically enforces that you have exclusive access to a collection to mutate it. This prevents also having an active iterator. You also have languages using immutable or persistent data structures in their std lib to side-step the problem.
- kriiuuu 2y agoThat is not UB. That is simply mutable data. The solution here is static analysis (Rust) or immutable persistent collections.
- MindSpunk 2y agoDepending on your collection iterator invalidation _is_ UB. Pushing to a vector while iterating with an iterator will eventually lead to dereferencing freed memory as any push may cause the vector to grow and move the allocation. The standard iterator for std::vector is a pointer to somewhere in the vector's allocation when the iterator is created, which will be left dangling after the vector reallocates.
- pjmlp 2y agoIn the context of C++ and STL it is UB. They are in the process of rewording such cases as erroneous instead of UB, but it will take time.
- saagarjha 2y ago…what’s the difference?
- tsimionescu 2y agoIf it is UB, the compiler is allowed to optimize based on the assumption that it can't happen. For example, if you have an if in which one branch leads to UB and the other doesn't, the compiler can assume that the branch that led to UB will never happen and remove it from the program, and even remove other branches based on "knowing" that the branch condition didn't happen. If it's simply erroneous, then it behaves like in every other language outside C and C++: it leaves memory in a bad state if it happens at runtime, but it doesn't have any effect at compile time.
- saagarjha 2y agoWhat exactly does "a bad state" mean?
- thayne 2y agoIt isn't so much that it is unintuitive, for the most part[1], but rather that there are a lot of things to keep track of, and a seemingly innocous change in one part of the program can potentially result in UB in somewhere far away. And usually such bugs are not code that is blatantly undefined behavior, but rather code that is well defined most of the time, but in some edge case can trigger undefined behavior. It would help if there was better tooling for finding places that could result in UB. [1]: although some of them can be a little surprising, like the fact that overflow is defined for unsigned types but not signed types
- atiedebee 2y agoI think the reason that signed integer overflow is undefined is that it wasn't uncommon at the time to have architectures that didn't represent signed integers with 2's complement. Even today you may find issues with signed integers. The ESP32 vector extensions seem to saturate signed integer overflow [1]. [1]: https://bitbanksoftware.blogspot.com/2024/01/surprise-esp32-s3-has-few-simd.html https://bitbanksoftware.blogspot.com/2024/01/surprise-esp32-...
- xelxebar 2y agoWould you mind sharing how you became sensitized to UB code? Did you just read the C spec, carefully digest it, and then read/write lots of C? Or do you have other recommendations for someone else interested in intuiting UB as well?
- brabel 2y agoI'm sorry but OP seems to be vastly overestimating their abilities. Every study about bugs related to UB show that even the best programmers will make mistakes, and often mistakes that are nearly impossible to have prevented without static tools because of the action-at-a-distance nature of the harder ones (unless you had the whole code base in your head, and you paid enormous attention to the consequences of every single instruction you wrote, you just couldn't have prevented UB).
- mgaunard 2y agoYou mean studies from Google, which explicitly has a culture of dumbing down software development, and heavily focuses on theoretical algorithmic skills rather than technical ones?
- brabel 2y agoGoogle hires the best developers in the world. They pay well beyond anyone else except the other big SV tech giants, who compete for the best. I don't work for them but if money was my main motivator and they had jobs not too far from me I would totally want to. My point is: don't pretend you're superior to them. You're very likely not, and even if you are really good, they're still about the same level as you. If you think they're doing "dumb" development, I can only think you're suffering from a very bad case of https://en.wikipedia.org/wiki/Dunning%E2%80%93Kruger_effect https://en.wikipedia.org/wiki/Dunning%E2%80%93Kruger_effect , without meaning disrespect.
- mgaunard 2y agoIgnoring the fact this isn't even true, you're completely misunderstanding my point. As I said, Google does not prioritize for technical expertise, primarily because that's quite individual-centric. They're a large organization and their goal is to make sure people are formatted and replaceable. They hire generally smart people and mold them to solve problems with the frameworks that they've previously built, with guidelines that are set such that anyone can come and pick it up and contribute to it, in order to facilitate maintenance in case of turnover or changing teams. They also hire a lot of people straight out of university, with many people spending large portions of their career there without seeing much of the outside world. As a result, their workforce is not particularly adept about using third-party tools in a variety of situations; they're optimized to know how things work at Google, which is its own sub- (or arguably mono-)culture. Being an expert requires using a tool in many diverse codebases for widely different applications and domains. A large organization like this is not a particularly good data point to assess whether people can become good experts knowledgeable about the gotchas of a programming language.
- tsimionescu 2y agoYou say this, and yet every single major project written in C has undefined behavior issues. The Linux kernel even demanded and uses a special flag in GCC to define some of this UB (especially the most brain dead one, signed integer overflow).
- uecker 2y agoThe linux kernel nowadays uses the fact that signed overflow is UB to detect problems using sanitizers. It turns out the defined unsigned wraparound is now the hard problem.
- littlestymaar 2y ago> but “ridiculous amount” and “difficult to avoid” is overstating it Maybe you can argue that C doesn't have a “ridiculous amount” of UB (even though the number is large), but C++ is so much worse I don't think saying it's “ridiculous” is off the mark. And not only the amount is already ridiculous but every new feature introduced in modern versions of C++ adds its own brand new UB!
- uecker 2y agoIf you count the number of UB in the standard, then yes, 200 cases is high. There is some ongoing effort to eliminate many of them. But it should also be noted, that almost all of those cases are not really problematic in practice. The problematic ones are signed overflow, out-of-bounds, use-after-free, and aliasing issues. Signed overflow is IMHO not a problem anymore because of sanitizers. In fact, I believe that unsigned wraparound is much more problematic. Out-of-bounds and use-after-free can be dealt with by having good coding strategies and for out-of-bounds issues I expect that we will have full bounds safety options in compilers soon. Aliasing issues can also mostly be avoided by not playing games with types. User-after-free is more problematic (and where the main innovation of Rust is). But having a good ownership model and good abstractions also avoids most problems here in my experience. I rarely have actual problems in my projects related to this.
- GTP 2y ago> Signed overflow is IMHO not a problem anymore because of sanitizers IIRC the overflow in SHA3's reference implementation was hard to catch also for ststic analisys tools, and had the practical impact of making it easy to generate collisions.
- uecker 2y agoA sanitizer could have transformed this into a run-time trap.
- littlestymaar 2y ago> Out-of-bounds and use-after-free can be dealt with by having good coding strategies You're basically saying that every project in the wild has bad “coding strategy”… > I expect that we will have full bounds safety options in compilers soon Which will be disabled in most places because of the overhead it incurs. > But having a good ownership model and good abstractions also avoids most problems here in my experience. I rarely have actual problems in my projects related to this. It's easier said than done when you have no way of enforcing the ownership, and practically intractable when not working alone on a codebase.
- uecker 2y agoI agree. I do not find UB very problematic in practice. It is still certainly a challenge when writing security sensitive code to fully make sure there is no issue left. (also of course, model checker, or run-time verified code such as eBPF etc. exist). But the amount of actual problems I have with UB in typical projects is very low by just some common sense and good use of tools: continuous integration with sanitizers, using pointers to arrays instead of raw pointers (where a sanitizer then does bounds checks), avoiding open coded string and buffer operations, also abstracting away other complicated data structures behind safe interfaces, and following a clear policy about memory ownership.
- anon-3988 2y agoI've spent hours debugging a memory alignment issues. Its not fun. The problem is that you don't know (at first) the full space of UB. So you spend the first 10 years of programming suffering through all kinds of weird UBs and then at the end of the pipeline claims "pftt, just git gud at it. C is perfect!".
- layer8 2y agoMaybe I got lucky, because on my first C job I got told to make sure to stick to ISO C (by which they probably mostly meant not to use compiler-specific extensions), so I got down the rabbit hole of reading up on the ISO specification and on what it does and doesn’t guarantee. Making sure you have no UB certainly slows you down considerably, and I strongly prefer languages that can catch all non-defined behavior statically for sure, but I don’t find C to be unmanageable. Memory alignment issues only happen when you cast pointers from the middle of raw memory to/from other types, which, yes, is dangerous territory, and you have to know what you are doing there.
- barkingcat 2y agoif anything, I think the wording “ridiculous amount” of UB in the context of C is understating things.
- deaddodo 2y agoI think the biggest problem is people conflating "undefined" with "unknowable". They act like because C doesn't define the behavior you can't expect certain compilers to behave a certain way. GCC handles signed overflows consistently, even though the concept is undefined at a language level; as goes many other UBs. And the big compilers are all pretty consistent with each other. Is it annoying if you want to make sure your code compiles the same in different compiler sets? Sure, but that's part of the the issue with the standards body and the compiler developers existing independent of each other. Especially considering plenty of times C/C++ have tried to enforce certain niche behaviors and GCC/ICC/Clang/etc have decided to go their own ways.
- tsimionescu 2y agoThis is dead wrong, and a very dangerous mindset. All modern C and C++ compilers use the potential of UB as a signal in their optimization options. It is 100% unpredictable how a given piece of code where UB happens will actually be compiled, unless you are intimately familiar with every detail of the optimizer and the signals it uses. And even if you are, seemingly unrelated changes can change the logic of the optimizer just enough to entirely change the compilation of your UB segment (e.g. because a function is now too long to be inlined, so a certain piece of code can no longer be guaranteed to have some property, so [...]). Your example of signed integer overflow is particularly glaring, as this has actually triggered real bugs in the Linux kernel (before they started using a compilation flag to force signed integer overflow to be considered defined behavior). Sure, the compiler compiles all signed integer operations to processor instructions that result in two's complement operations, and thus overflow on addition. But, since signed integer overflow is UB, the compiler also assumes it never happens, and optimizes your program accordingly. For example, the following program will never print "overflow" regardless of what value x has: int foo(int num) { int x = num + 100; if (x < num) { printf("overflow occured"); } return x; } In fact, you won't even find the string "overflow" in the compiled binary, as the whole `if` is optimized away [0], since per the standard signed integer overflow can't occur, so x + 100 is always greater than x for any (valid) value of x. [0] https://godbolt.org/z/zzdr4q1Gx https://godbolt.org/z/zzdr4q1Gx
- 2y ago
- nurettin 2y agoThis is about asking the compiler for a constexpr and receiving a runtime evaluation, not ownership semantics.