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.