I'm not so sure whether Rust's strong immutability/exclusivity guarantees are worth the trouble though. Unexpected mutation hasn't been a major source of bugs or mental burden for me in other languages, at least not in a single threaded context.
I'm not so sure whether Rust's strong immutability/exclusivity guarantees are worth the trouble though. Unexpected mutation hasn't been a major source of bugs or mental burden for me in other languages, at least not in a single threaded context.
For the multi threaded context it's amazing. Compile-time data-race checking is such a unique and useful feature. That and the great ergonomics of the locking primitives makes multi-threaded programming in rust really enjoyable.
I think this is a really underrated benefit of the ownership model. You cannot forget to acquire a lock, and you cannot keep a reference to a lock's contents past the point where you've released it.
Oh, it has for me. In fact, unexpected mutation has been one of the most difficult bugs I've encountered.
I had a method, foo, which when given a NavigableMap (java), it would throw an exception. However, if you called the same method with the same Map, no exception would be thrown (because the map was mutated deep down).
Turns out, in the 84th circle of hell after a sub-sub-sub map view was created. A cute little said something like 'if key doesn't exist, associate key with 0'.
Because NavigableMap views are mutable in java this eventually made it's way all the way back to the parent map which, consequentially, caused the map to go down a different path when the same method was re-invoked.
It took a very long time to ultimately find that mutation due to the complexity of the code.
That's the beauty of Rust: it assumes single-threaded programs don't exist.
All types — including all 3rd party libraries — are forced to be either thread-safe or explicitly not compile if they'd cross thread boundaries. Nobody has "but I don't support multi threaded programs" excuse, so you may as well throw multithreading at everything.
let nut v: Vec<T> = ...;
let r: &T = &v[0];
v.push(...); // A
foo(r); // B
The reference passed at B might be invalidated by A, if the vector is reallocated. It’s the shared vs exclusive borrowing that defends against this, by flagging the above program as an error.Could this not be covered by the lifetime guarantee without imposing read/write exclusivity on all values even where lifetimes are unaffected?
It's a good example, but I'm still not enirely convinced that the implication is necessarily correct.
I wonder if a sufficiently smart compiler could not detect this particular special case and relax immutability/exclusivity in other cases where no references to the internal structure of a value exist, or if it could even keep whatever r is referencing alive to uphold the lifetime guarantee.
I realise the latter could have some undesirable side-effects wrt memory usage and it raises ownership questions if there are multiple such references. It's essentially what persistent data structures in functional languages do and it may not be a good fit for Rust.
The reason why I'm even interested in this is that Rust can sometimes feel restrictive in situations where having both mutable and immutable references or more than one mutable reference to the same thing is reasonably safe, such as in the local scope. Obviously people are working around mutability restrictions by using indexes/handles instead of references, but that doesn't seem any safer.
1. In order a sufficiently smart compiler to understand if this is okay, it would have to know how the Vec<T> type actually works. But Vec<T> is a library type, and uses unsafe code, which is kind of definition-ally code that the compiler cannot understand. So, even for this specific case, it is too hard to do so today. A human cannot even tell if this is okay or not, right now, because you'd need to see the code that's in the `...`.
2. Even if it could, it's unclear if it's a good idea. The more complex analysis a compiler can do, the harder it is to explain what it's doing to end-users. Imagine that, for example, we wrote this code, with the `...` being a case where it's safe, but then we modified it to a case where `...` was not safe anymore. What would that diagnostic look like? How could it be communicated to people in a useful way?
3. There's a tension here. If you make it work for very specific reasons, any small changes would likely cause it to break, whereas it breaking up front may lead you toward a design that is overall more robust.
> It's essentially what persistent data structures in functional languages do and it may not be a good fit for Rust.
We have those! https://crates.io/crates/im
> Rust can sometimes feel restrictive in situations where having both mutable and immutable references or more than one mutable reference to the same thing is reasonably safe, such as in the local scope.
It depends! You're not wrong, but also see my comment over here: https://news.ycombinator.com/item?id=22466587 even local scope may not be enough.
Do any languages somehow use their GC to update references when a growable array is reallocated? I'm not aware of any language that does.
Furthermore, its hard to see how it could. In the case of a simple copying/compacting collector the whole world is stopped and the pointer graph traced. But you obviously can't do that every time a growable array needs to be reallocated. I understand that there are more complex solutions that avoid that pause, but still not any I'm aware of that would be exploitable for reallocating an array.
Please don’t spread FUD. The majority of commercial C++ software out there is rock solid.
Vulnerabilities are the actual problem in today's Internet connected world and why Rust has grown so fast.
As for GCs: they are not good enough for some fields, so they cannot be used. Even state of the art ones like Go's.
Go's GC is a variation of a regular mark and sweep GC: the GC starts exploring the heap to find all reachable memory, and then clears the unreachable one, meanwhile compacting GCs move the reachable memory from one place (the “from space”) to another (the “to space”), which become the new heap. Theses two garbage collectors famillies are completely different.
BTW, calling go's GC “state of the art” is debatable at best. See : https://blog.plan99.net/modern-garbage-collection-911ef4f8bd...
Go as a language was designed like that, and special attention was made to allow as much value types as possible, to allocate as much things as you can on the stack, thus reducing GC pressure. For the first five years of Go or something, Go's GC was actually pretty bad (it was probably the most basic GC you could find in any somewhat popular language), but it wasn't too much of a deal because you can avoid it most of the time when it goes in your way (much more easily than in Java for instance).
After some time, they decided to enhance it, but they were on a budget (no lots of money spent on it” actually), so because in go you can avoid allocating memory on the heap, they decided to focus on GC latency instead of throughput (if the GC's throughput isn't good enough for you, you better reduce your allocations).
Overall go is a pretty fast language, and it's an engineering success, but it's in spite of its GC and thanks to other parts of the language's design, not because Go's GC is exceptionally good (it's not, and if you read my link you'd understand how).
Since I don’t have a sense on the timescales, I will take your word for it that it was the reverse.
2. Windows was (is?) not coded in modern C++ but a bastard C or very old C++ from what I understand. Bugs were also mainly by third-party drivers and software. The industry for home user software had very bad quality, that’s is true, but that had nothing to do with C++. C, Fortran, Delphi and others were used too.
3. Videogames are in an industry where deadlines are more important than bugs and companies really don’t care about fixing stuff before release.
https://msrc-blog.microsoft.com/2019/07/18/we-need-a-safer-s...
https://security.googleblog.com/2019/08/adopting-arm-memory-...
https://kernsec.org/wiki/index.php/Kernel_Self_Protection_Pr...
https://developer.apple.com/swift/#safety
Modern real time GC are good enough for military use, https://www.ptc.com/en/products/developer-tools/perc
For instance, you can have a library that works perfectly for any non-malicious/valid PNG image, yet have vulnerabilities for invalid, crafted files.
In fact, this is almost always the case for any product. Only really bad quality software has issues with the happy expected path. But many more will have vulnerabilities in the unexpected paths.
It has been my experience, that even the well known softwares written in C (not sure about C++) are indeed kind of crashy. Example: Open Broadcast Studio, used by many people to stream on various platforms. It is possible there to go into the settings and change them so that OBS crashes and cannot be restarted again, unless you guess, that the configuration is faulty and OBS is unable to deal with a faulty config. You can only get it to run again by removing the config or reinstalling. Great quality. VLC is another example of such crashy software. Not only did it crash many times in the past, but also cannot properly deal with unplayable files in a looping playlist and will simply bring all your cores to 100% usage.
So far some anecdotes from the well known C software world. I am not sure, whether the people in the respective community consider these softwares to be good quality.
But in many cases I'm using APIs designed by others. Most older libraries come with their own idiosyncratic memory management patterns.
Even the standard library has lots of different ways of passing reference-like things around. E.g.
https://en.cppreference.com/w/cpp/string/basic_string_view/b...