Despite appearances, storing the reference to an object stored in a mutable vector is not safe. Vectors can grow; once a vector's length matches its capacity, it can only grow by allocating a larger array and moving all objects inside it to the new location. Existing references to its elements would be left dangling, so Rust doesn't allow that. (Also, a vector can be shrunk or cleared, in which case any references to its elements will obviously point to deallocated memory.) The same problem would exist with a C++ std::vector
.
There are several ways around it. One is to switch from a direct reference to Cell
to a safe back-reference that consists of a back-pointer to the Game
and an index to the vector element:
struct Player<'a> {
game: &'a Game,
cell_idx: usize,
}
impl<'a> Player<'a> {
pub fn new(game: &'a Game, cell_idx: usize) -> Player<'a> {
Player { game, cell_idx }
}
pub fn current_cell_name(&self) -> &str {
&self.game.cells[self.cell_idx].name
}
}
Compilable example at the playground.
That has the downside that it doesn't allow adding cells except by appending them, because it would invalidate players' indices. It also requires bounds-checking on every access to a cell property by Player
. But Rust is a systems language that has references and smart pointers - can we do better?
The alternative is to invest a bit of additional effort to make sure that Cell
objects aren't affected by vector reallocations. In C++ one would achieve that by using a vector of pointers-to-cell instead of a vector of cells, which in Rust one would use by storing Box<Cell>
in the vector. But that wouldn't be enough to satisfy the borrow checker because shrinking or dropping the vector would still invalidate the cells.
This can be fixed using a reference-counted pointer, which will allow the cell to both survive the vector growing (because it is allocated on the heap) and shrinking so it no longer includes it (because it is not owned exclusively by the vector):
struct Game {
is_running: bool,
cells: Vec<Rc<Cell>>,
}
impl Game {
fn construct_cells() -> Vec<Rc<Cell>> {
["a", "b", "c"]
.iter()
.map(|n| {
Rc::new(Cell {
name: n.to_string(),
})
})
.collect()
}
pub fn new() -> Game {
let cells = Game::construct_cells();
Game {
is_running: false,
cells,
}
}
// we could also have methods that remove cells, etc.
fn add_cell(&mut self, cell: Cell) {
self.cells.push(Rc::new(cell));
}
}
At the cost of an additional allocation for each Cell
(and an additional pointer dereference from Game
to each cell), this allows an implementation of Player
more efficient than index:
struct Player {
cell: Rc<Cell>,
}
impl Player {
pub fn new(cell: &Rc<Cell>) -> Player {
Player {
cell: Rc::clone(cell),
}
}
pub fn current_cell_name(&self) -> &str {
&self.cell.name
}
}
Again, compilable example at the playground.
cells
from the local variable into theGame
struct, you've invalidated any references into the vector, such as the one you've given toplayer
. Languages such as C or C++ would let you do that, AFAIK, and let the code crash at runtime. It's hard to tell what you really want to do, asplayer
is never used. – HawkweedCell
? You may want to search here on Stack Overflow for things along the lines of "parent / child relationships". If you want your code as it currently is written to compile, just add{}
aroundlet player = Player::new(&cells[0])
. – HawkweedPlayer
insideCell
- for example, a player may be required to switch to a different cell. Also, separation of concerns and implementation details ofPlayer
may require it to be defined separately. Either way, Rust is flexible enough to allow one to choose the tradeoff that makes sense in a particular program. I've now edited the answer to show both the index approach and the vector-of-boxes approach, both of which can be used to implement the kind of data structure you need. – Kantianism