3 ms·
Or I would say, programming in general is already hard. Rust makes it even more difficult… The reason is that actual common memory safety mitigation strategies
by cyber_kinetist 3y ago
Or I would say, programming in general is already hard. Rust makes it even more difficult…
The reason is that actual common memory safety mitigation strategies in real life involve using some kind of smart pointers (same with Rust and C++), or using indices with bound checking (same with Rust and C++, it’s just that C++ STL has this off in Release mode by default…). For both languages, trying to avoid these runtime mitigation strategies (which affects performance) will require doing some level of unsafe stuff which the compiler doesn’t help with you (the unsafe semantics of the two languages are actually similar since Rust relies on LLVM which relies on C semantics). So ultimately, the borrow checker doesn’t seem worth it if you actually think about the cognitive overhead and headaches it brings. The only reason Rust has an edge over C++ is that it has sane defaults and a better standard library (ah only if C++ had a sane way of initializing values…)
- hmfrh 3y ago> So ultimately, the borrow checker doesn’t seem worth it if you actually think about the cognitive overhead and headaches it brings. You quite literally also have to "borrow check" C/C++ in order to have a well formed program, just without any compiler assistance.
- cyber_kinetist 3y agoFor heap-allocated objects, there is a way to do borrow checks at runtime if you want (there's something called constraint references, explained in https://verdagon.dev/blog/raii-next-steps https://verdagon.dev/blog/raii-next-steps.) For structs that I do not want to heap-allocate, they're usually POD types and in arrays (which you can bound check), so there's not much to think about borrowing. The more concerning issue I usually have is about about initializing the values correctly (which usually Rust doesn't help, when reasoning about performance-sensitive code).
- estebank 3y ago> For structs that I do not want to heap-allocate, they're usually POD types and in arrays (which you can bound check), so there's not much to think about borrowing. Both situations which also apply to Rust. Whenever the borrow checker complains you can do exactly the same thing. > The more concerning issue I usually have is about about initializing the values correctly (which usually Rust doesn't help, when reasoning about performance-sensitive code). You can use a type-state machine, where the only way to construct the final value is by calling all of the appropriate methods that change the type parameters on the Self type. When that gets compiled it ends up as either a single memcopy of values, or you can make the Self type hold a MaybeUninit value to make the partial construction with no copy at the end possible. I actually implemented that for fun and as it turns out already existing crates that held every field in an Option and then built the final value from those ended up being faster. C'est la vie.
- MrJohz 3y agoOne of the things I found interesting about this paper is that they find that there were more contributors in Rust projects. The first headline I saw posted from this paper was that first time contributors to Rust projects are 70× less likely to introduce vulnerabilities than first time contributors to C++ projects, and my immediate gut reaction was that this could be explained if the Rust developers were generally more experienced programmers. But as the abstract points out: > We also found that the rate of new contributors increased overall after switching to Rust, implying that this decrease in vulnerabilities from new contributors does not result from a smaller pool of more skilled developers, and that Rust can in fact facilitate new contributors. Which seems to imply the opposite of what you're suggesting, i.e. it implies that Rust makes contributing easier and safer than C++. I also want to point out that your comment isn't accurate about how Rust's unsafe works: > [T]rying to avoid these runtime mitigation strategies [...] will require doing some level of unsafe stuff which the compiler doesn’t help with you (Emphasis my own.) This is inaccurate — the borrow checker is not turned off inside of unsafe blocks, but instead you are given a handful of extra APIs to use. Those extra APIs can be used to do things that the borrow checker would not normally allow (e.g. by converting checked pointers into raw pointers, and then inside the unsafe block, dereferencing those raw pointers), but the borrow checker is still active, and still catches all the same errors as before.
- cyber_kinetist 3y ago> the borrow checker is not turned off inside of unsafe blocks, but instead you are given a handful of extra APIs to use. Those extra APIs can be used to do things that the borrow checker would not normally allow (e.g. by converting checked pointers into raw pointers, and then inside the unsafe block, dereferencing those raw pointers), but the borrow checker is still active, and still catches all the same errors as before. Yup I know that the borrow checker does also work inside unsafe blocks. But when what you primarily do as a systems programmer is establishing invariants in your systems so that you can exploit (or circumvent) the UB-ness of the underlying system (OS/driver/hardware/etc.), and when the borrow checker doesn't really help you with verifying these invariants... The Rustonomicon states that whenever you open up an unsafe block, it doesn't pollute only the scope, but the whole module (https://doc.rust-lang.org/nomicon/working-with-unsafe.html https://doc.rust-lang.org/nomicon/working-with-unsafe.html). So there can be a correctness issue outside the unsafe block because it disobeys the invariants implicitly set up by the developer inside the unsafe block. And you can't do anything about this other than carefully reasoning about all the various ways your implicit invariants can break inside the whole module. So unsafe is ultimately more of a convention (or a promise) that you have designed and verified your invariants correctly, so that you will not produce undefined behavior no matter how you use the module as an outside user. If you want to verify your invariants any further than that, you need to check UB at runtime using the Miri interpreter (which is really slow and still incomplete), or just use Ada SPARK.