5 ms·
The "why is this an issue" has already been mentioned ("what if `self.phrase.clear()` is called? that creates dangling references in self.words"), but how to so
by volta83 5y ago
The "why is this an issue" has already been mentioned ("what if `self.phrase.clear()` is called? that creates dangling references in self.words"), but how to solve it is relatively straight forward:
struct Input {
words: Vec<(usize, usize)>,
phrase: String,
}
impl Input {
fn word(&self, i: usize) -> &str {
let (first, last) = self.words(i);
&self.phrase[first..last]
}
}
Now your Input type doesn't even need to be generic over the reference 'a anymore (not that it mattered before because that wouldn't work).
Now even if self.phrase is resized, you never get memory unsafety, because the `self.phrase[...]` does a bounds check and panics on out-of-bounds. So the worst you can get is a "logic error" in your program that shows up with a really nice backtrace that's super easy to debug, instead of a miscompiled program (due to UB) that's often very painful to debug.
So sure, you don't get "self referential struct with references" in Rust. But so what?
The fix for when you want this is always the same: use offsets like I did above. Offsets are much better than any kind of pointer, because they allow you to just "memcpy" the struct around (the address of a field changes on move and copy, but the offset to that field from the beginning of the struct always remains constant).
If you were to use a pointer to a field, you'd need to correct that pointer during copies and moves. C++ allows this, and this "feature" has many undesirable consequences for Rust:
- moves in Rust are O(size_object), moves in C++ can be of any algorithmic complexity, so any algorithm that moves has to take that into account this
- rust collections have one code path, C++ collections have two code-paths: one for objects that are "simple" to move, and one for objects that are "hard" to move
- move constructors that throw in C++ interact with exception safety in complicated ways
- probably many many more interactions that I'm missing here
There might be ways to improve the ergonomics of self-referential structs in Rust, but given that offsets are simple and always work correctly without many downsides, whatever improvement one considers for Rust would probably not be worth it if it has any of C++ downsides.
- zozbot234 5y ago> If you were to use a pointer to a field, you'd need to correct that pointer during copies and moves. The "transfer" crate allows for this, though not in a way that's directly compatible with C++ move. (The latter has been discussed somewhat in https://mcyoung.xyz/2021/04/26/move-ctors/ https://mcyoung.xyz/2021/04/26/move-ctors/ but that's only a first look at the problem area, not aiming for 100% accuracy just yet.)
- cesarb 5y agoAnother bonus of the offset approach: it can be smaller. The size of a &str will always be two words (pointer and length), but if you're sure that your strings will never be longer than 4 gigabytes, and you're willing to put up with the necessary integer casts, you can store a (u32, u32), which is half the size of a (usize, usize) or a &str when your target uses 64-bit words. And if you're certain that your strings are never bigger than 65535 bytes, you can go even further and use a (u16, u16), and so on.
- wokwokwok 5y agoIf you’re applying arbitrary invariants that aren’t codified in the type system, you may as well just use raw pointers. Offsets cause bugs when you do things like assume your strings are of a particular length; that’s why they’re a bad choice. Use a crate that provides a safe abstraction, don’t roll it by hand yourself, and the safe abstraction can be implemented in whatever way you want; but it’s not safe if it doesn’t enforce the invariants.
- kd5bjo 5y ago> If you’re applying arbitrary invariants that aren’t codified in the type system, you may as well just use raw pointers. This isn’t really true, as the character of bug produced is quite different between the two cases. If you make a mistake with offsets, you’ll get a runtime panic that tells you exactly where the problem occurred. If you make the same mistake with raw pointers, however, you’ll get corrupted memory somewhere which won’t necessarily show up as a problem until long after the erroneous code is executed.
- wokwokwok 5y agoWho honestly cares? I sure don’t. Yes, there is a distinction, yes one is worse than the other, but if you got a panic, your application terminated. You’re screwed either way, web server or desktop app; fortunately there’s a simple solution: use whatever means you like to write a safe abstraction that enforces invariants. I’m not advocating particularly for unsafe code; but avoiding unsafe and writing code that blatantly panics at the drop of a hat is ridiculous. Invariants should be enforced by the type system. I don’t care how; if you’re not doing that and just assuming your strings are shorter than 256 characters, you’re writing code that is unsound.
- petmon 5y agoHow would you adjust this approach if the words needed to be keys? For example, if words were a map from a word to its count, and you could get the count for a &str?