I'm a C/C++ developer learning Rust - so this question may be due to my ignorance at this point. Why doesn't Rust relax the data-race preventing borrow rules on a type that is marked !Send + !Sync
? There are many types that are not intended to be shared across threads, and it would be nice to be able to work with multiple mutable references to instances of such types without using unsafe
directly or indirectly. The lifetime rules that prevent dangling references would still hold.
The exclusiveness of &mut
isn't just about data races but about invariants of the data structure being temporarily violated, which can lead to bugs and UB in single threaded programs. Iterator invalidation is the classic example, but there are others.
As a C++ developer I imagine you are already aware of the dangers of what you are suggesting. For example, Iterator Invalidation in C++ - GeeksforGeeks
Because borrow rules are entirely unrelated to Send
or Sync
. They are used to guarantee correctness of your program even if it's single-threaded.
The fact that you may add marker traits like Send
and Sync
and ensure that your multithreaded program doesn't have data races is just a nice bonus.
You can use RefCell for that (there are other cells, too). This shifts validation of exclusiveness from compile-time to runtime, though.
Don't think about &mut
as about mutable unique reference. Think about mutable unique reference.
Interior Mutability is a thing, thus you may use shared references to access and modify some resources, but unique reference is unique, end of story.
There was even an attempt to switch to &uniq before Rust 1.0 release but it happened to late for that to happen.
And as someone who worked with C++ for a long time this approach felt like I'm just continuing to use normal C++ rules, just now compiler has my back.
Phew, that was close. &uniq
is really ugly.
I just want to point out that Cell<T>
doesn't rely on runtime checks. Instead, it prevents references to its interior when shared. That makes it very useful for values you want to copy, swap or overwrite.
Oh. What I didn't understand is that Rust is trying to prevent aliasing issues. Now that I'm searching on aliasing, I can see that. Something I read had me thinking that the borrow checker rules were just for data race and dangling reference prevention.
They're strongly related though, aliasing and dangling references. Aliasing isn't prevented for preventing aliasing's sake. Of course, shared mutability is also a recipe for running into logic errors, Rust does allow it as far as possible with opt-in solutions; and you might be able to notice that even those opt-in solutions for shared mutability (without synchronization), namely Cell
and RefCell
, arenāt a straightforward āless strict borrowingā solution. Instead, RefCell
introduces dynamic aliasing checks and guard objects, whilst Cell
prevents by-reference access to the contained value alltogether; each carefully avoiding any APIs that could make Rust no longer memory-safe.
A maximally simple demonstration of dangling reference from (bad) aliasing would be as follows:
let mut boxed_value: Option<Box<i32>> = Some(Box::new(42));
let mut_ref: &mut Option<Box<i32>> = &mut boxed_value;
let aliasing_ref: &Option<Box<i32>> = &boxed_value;
// Rust compiler will prevent us from using both mut_ref and aliasing_ref,
// but what if it didn't (and we aren't using threads, anyway)
// first get a pointer to the interior
let inner: &i32 = aliasing_ref.as_deref().unwrap();
// then mutate/invaliade the containing box
*mut_ref = None;
// of course, this is also similar in spirit to iterator invalidation
// use now-dangling reference
println!("{}", *inner);
Imagine, we did allow mutation of Option<Box<i32>>
through a non-&mut
reference, then the above example could be followed to run into a case of using a dangling reference, a simple use-after-free essentially.
All memory safe programming languages that do allow for significantly less restricted shared mutability than Rust will need to find a different way to avoid problems like this. The most common approach is generally to remove the ability to do zero-overhead dereferencing like Rust (or C/C++, but those arenāt memory-safe languages) allow, i.e. the line
let inner: &i32 = aliasing_ref.as_deref().unwrap();
Rust can allow you to create a reference to something behind an indirection (such as Box
) without any overhead, where most āhigher-levelā memory-safe languages today would only allow such a projection if the resulting reference to the contained i32
comes with additional bookkeeping that dynamically prevents the Box
to become de-allocated while the reference is held. (E.g. by means of this reference being known to a garbage collector; or by incrementing some reference count.)
Is there a "How to think about the philosophy of Rust for C/C++ die-hards" intro somewhere? Because if the Rust philosophy is consistently "Make the most safe/efficient way the default, and provide explicit workarounds when that's not enough, where the workarounds are specific and carefully targeted, not all-or-nothing", then learning that early would help us C/C++ die-hards out of the "But why are they tying my hands like that?" mindset.
Aliasing is one of those things that, as a C/C++ developer, you're just used to dealing with and don't consider problematic even though of course it is. You probably wouldn't expect to be saved from it unless you switch to a purely functional language. It's still a problem in imperative languages that are GC-based. If you were conceptualizing Rust philosophy as "How to get the safety of GC-based imperative languages without GC, and the safety of CSP-style concurrency without forcing message passing" (in other words, Go for system programmers), then you were wrong (or at least incomplete).
I wouldn't say it conveys the whole philophy, but it does feature good showcases of some of Rust's most important safety concepts[1] and it's an intro "for C++ programmers": Feel free to watch this presentation if you haven't already, I personally like this video it a lot.
including a section on aliasing, and even on how it's related to dangling references ā©ļø
The video linked above is a good start. I think I have seen similar presentations along the lines of "Rust for C++ devs" on YouTube.
I'd be inclined to suggest that one way of looking at the the Rust philosophy is that it is intended to be a language as powerful, performant, and expressive as C/C++ class languages but without the possibility of writing something that will cause undefined behaviour at run time. To be a "systems programming language", which to my mind means capable of writing operating systems, kernels, compilers, as well as everything else one can do in Rust (which is everything).
Competing with garbage collecting languages is besides the point. There is no competition.
With that in mind I suggest that to see the Rust philosophy have a look for all the times "undefined behaviour" is mentioned in the C++ standard and think "we are not going to have that in Rust". Have a look at the thousands of suggestions and rules in various C++ coding standards documents, many of which are about avoiding unsafe memory use and UB, and think "That's nice, in Rust I don't have to remember all those rules and apply them to every line of code I write, the compiler does it for me.
More precisely, "we are not going to have that in safe Rust". And the philosophy with unsafe
Rust then is that - as far as it's possible - that the things it is good for should be made available behind a safe interface, anyways.
Like with aliasing for example, we have the normal aliasing rules around &T
and &mut T
, then there is the unsafe opt-out wrapper UnsafeCell<T>
where &UnsafeCell<T>
allows for shared mutation of the contained T
, but if you don't pay attention, there are all sorts of ways your aliasing issues can run into UB[1], and then, many of the possible strategies to rule out undefined behavior from shared mutability once again, are implemented with various safe wrappers around UnsafeCell<T>
, like Mutex<T>
/RwLock<T>
[2], Cell<T>
[3], RefCell<T>
[4], etc...
in the same way as in C++; however, Rust is often even more restrictive on what kinds of abuse result in UB; especially creating aliased
&mut T
already creates UB ā©ļøenforcing no aliasing at runtime, in a thread-safe manner ā©ļø
which is zero-cost without any dynamic checks; but for soundness it's preventing multi-threaded access with
!Sync
and preventing by-reference access to the interior by not offering anything like that in its API ā©ļøenforcing no aliasing of mutable references at runtime, more cheaply than
Mutex<T>
, but not thread-safe, preventing multi-threaded access with!Sync
ā©ļø
Yes indeed. I decided against mentioning that caveat.
Thing is, without "unsafe" a language is useless. It could not do any I/O, it could not make system calls to an OS, it would not have any FFI. It would be a sand box with no exit.
So we have to have "unsafe" and it's best that it is confined and we know where it is. And it's mostly wrapped in safe interfaces so applications need not be concerned with it.
You can also think of &mut
references to always be restrict *
, unless Unlike CPP, Rust forces you to prove that the pointers actually don't overlap.UnsafeCell
is involved.
Honestly, I really do wish the latter thinking was less prevalent. Your hands were never tied. There's always an escape hatch. (Well, maybe not always! There are definitely some unfortunate gaps and shortcomings in the language, but Mutability XOR Aliasing isn't one of them, IMHO.)
I also wish I had some better suggestions for an intro than "just keep learning!" Rust is both broad and deep. The problem space might just be too big to cover in a single intro. And the Cliff's Notes can be found in the usual places (The Book, The Reference, The Nomicon, etc.)
Premature apologies, but this part of my reply turned into a bit of a rant!
From my perspective, that is exactly the mental model of the Rust safety guarantees to a "C++ diehard." And I think nobody demonstrates this better than Herb Sutter's "C++ safety, in context" where he argues:
- "I just want C++ to let me enforce our already-well-known safety rules and best practices by default, and make me opt out explicitly if thatās what I want." (emphasis added)
- "... One reason [for its fundamental incompatibility with C++] is that Rustās safe language pointers are limited to expressing tree-shaped data structures that have no cycles; that unique ownership is essential to having great language-enforced aliasing guarantees, but it also requires programmers to use āsomething elseā for anything more complex than a tree (e.g., using Rc, or using integer indexes as ersatz pointers); itās not just about linked lists but those are a simple well-known illustrative example." (emphasis added) [1]
My natural follow-up questions are: Why should C++ get an opt-out but Rust shouldn't? Why limit Rust to safe references, but not whatever they plan to introduce in C++?" The reality is that Rust does have an opt-out for this already, and it's shown in the standard library documentation. It isn't strictly the safe subset of Rust [2], but that's my point.
What he wants in C++ for memory safety already exists in Rust. That he sets up a strawman with Rust's lifetime-tracked references is at best missing the point, and at worst is completely misleading. It makes Rust appear "lesser than C++" to those who don't know enough to say, "wait a minute Mr. Sutter, that's a false equivalence!"
Or, to summarize, there are a lot of moving parts. Any attempt to reduce the safety guarantees of the language to the safe subset is missing out on a wealth of opportunity from the unsafe
superset. [3]
This quote is taken out of context slightly. Herb is arguing that Rust's type system is fundamentally incompatible with C++. But I think this specific example is just outright wrong. If the example were something about ABI stability or RTTI or some other niche C++-ism, I might be less peeved overall. But this presents itself as fodder for supporting arguments that say C++ is obviously better than Rust because "cycles are impossible" or whatever nonsense. ā©ļø
The
Pin
example instd
requires dereferencing a raw pointer to do anything useful with it.unsafe
is required for the dereference. Ergounsafe
is akin to the original "opt out explicitly if that's what I want" requirement. ā©ļøIt's just that most users of the language never have to use the full superset directly, and there is plenty of social pressure to avoid it where it's unnecessary. ā©ļø
And I think this the main difference between C++ and Rust. And the reason why that that subset of a superset approach would never work.
It's not that C++ language couldn't be made safe. It possible. Ada people did that (by lifting rules from Rust, of course, but Rust doesn't mind).
But that āI know what I'm doingā mentality (glorified here or here and in many other places)? Where we have bunch of people who think that every program which ābreaks the rules for good reasonā is supposed to be, somehow, handled by compilers even if no one may have any idea how that's possible on one side and bunch of people who think that any violation of rules, not matter how minor gives them the right to silently corrupt everything everywhere?
That may only be fixed in an old-fashioned way: An important scientific innovation rarely makes its way by gradually winning over and converting its opponents: it rarely happens that Saul becomes Paul. What does happen is that its opponents gradually die out, and that the growing generation is familiarized with the ideas from the beginning.
It's currently an UB but there are an ongoing duscussion about maybe relacing that requirement. Which is shows that main social difference very clearly.
List of UBs defined by the language is a contract. Like any contract it may be changed. But as long as it's in effect everyone should try to uphold it's requirements to the best of their abilities.
In C/C++ landā¦ that simple idea is, somehow, rejected categorically. C/C++ developers break UBs left and right without even a tiny hint of remorse. And C/C++ compiler writersā¦ they use UB list from the standard as a carte blanche which gives them the right to break anything and everythingā¦ stance which would have been justified if C/C++ compiler writers themselves would used that holy text of the standard in the same fashion. But no, that's not what is happening, provenance was introduced into the language twenty years ago via defect report venue, but since standard doesn't mention itā¦ how many C/C++ developers know about it?
Compare to the situation in Rust: not only reference very explicitly tells you that definitions of rules are still in flux, but there are also one propsal, another one plus bunch of practically usable ideas about how to handle that.
Everyone agrees that current situation when rules are not finalized is far from being ideal and ideas how to handle that are discussed but people are trying to develop a solution which would satisfy everyone.
In C/C++ land? Everyone is firmly believed that they shouldn't do anything and these other people should do something to fix everything. This would just never work.
And this brings us to this:
Surprisingly enough Rust started with this that idea. This was really what was defining Rust in the beginning. But thenā¦ these goals were altered and rules have changed, too. Graydon Hoare (they guy who started Rust originally) wrote about this here. That's why we don't have this:
Maybe you, or someone else, would create a video or a blog post which would summarize it better. But Rust is basically the result of an attempt to create a low-level language which provides safety by concerted effort of language developers and language users.
I don't think that discussion is about relaxing the aliasing requirements of &mut T
at all. It's about relaxing the requirements of the target of &mut T
to be containing a valid T
value (properly initialized, not itself a dangling or improperly aliased reference, etc...). As far as I understand, this shouldn't be discussing any of the aliasing requirements with regards to (other references to) the immediate target of the &mut T
reference.
That Firehose youtube link was great! An absolutely exceptional presentation. Thank you for the link, @steffahn.
This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.