Reference to element in vector
Asked Answered
D

1

19

I'm having trouble giving a struct a member variable that is a reference to another type. Here is my struct and implementation:

struct Player<'a> {
    current_cell: &'a Cell,
}

impl<'a> Player<'a> {
    pub fn new(starting_cell: &'a Cell) -> Player<'a> {
        Player { current_cell: starting_cell }
    }
}

The player has a reference to the current Cell that they are in. Here is my Game struct and its implementation:

struct Game {
    is_running: bool,
    cells: Vec<Cell>,
}

impl Game {
    pub fn new() -> Game {
        let cells = construct_cells();
        let player = Player::new(cells[0]);

        Game {
            is_running: false,
            cells: cells,
        }
    }
}

cells is a vector of Cells. When I create the game, I create a vector of cells in construct_cells() and then I start the player at the first cell. The error I am getting is:

expected &Cell, found struct `Cell`

I can see that I'm not passing a reference when I create the Player, but if I change the parameter to &cells[0] then it yells at me for borrowing the entire vector and then trying to use it again when I create the Game struct. So what's going on? How do I just give the player a reference to a Cell?

Donnydonnybrook answered 29/11, 2016 at 20:24 Comment(6)
Do you have a good understanding of what references (or pointers) are, specifically what a dangling reference (or pointer) is? What would happen if you gave a friend a piece of paper with your address on it (123 Main St.) and then you moved? How would the new owners of the house feel if your friend just showed up and started talking to the people in the house you used to live in as if they were you?Hawkweed
@Hawkweed I know what a dangling pointer is. I guess I just didn't realize that this solution could allow for one. How would I obtain the same result, then? Is what I am trying to do possible?Donnydonnybrook
I just didn't realize that this solution could allow for one — and that's why Rust is a wonderful language, IMO. When you move cells from the local variable into the Game struct, you've invalidated any references into the vector, such as the one you've given to player. 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, as player is never used.Hawkweed
@Hawkweed Well, right now, I want to add a function to the Player that just prints out the name of the Cell that they are currently in. My Cell struct has a name String and that works fine, but I need to actually get the Player to know what room it is currently in first. My instinct was to just give the player a reference. Also, after I create the cells array and return it, doesn't it move the ownership to the Game?Donnydonnybrook
just give the player a reference — why? Said another way: why not just put the player inside the Cell? 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 {} around let player = Player::new(&cells[0]).Hawkweed
There can be good reasons not to put Player inside Cell - for example, a player may be required to switch to a different cell. Also, separation of concerns and implementation details of Player 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
K
35

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.

Kantianism answered 30/11, 2016 at 19:56 Comment(5)
One thing that wasn't immediately obvious to me: storing a "true" (not index-based) reference to a vector's item requires borrowing the entire vector, making it impossible to mutate the vector (e.g. clearing it thus invalidating the reference) or any of its items.Dissertate
I'm not sure why this answer mentions mutable vectors - OP doesn't have a mutable vector. The second solution works not because of the Boxes (they're irrelevant - It also compiles without them), but because you give player a reference to the Cell after they've already been moved into Game, where as OP gives player a reference first, then tries to move cells.Irreclaimable
Although I'm also very curious about how share references to elements of a mutable vector. However, Boxes don't seem to help with that either. If we 1. create a mutable Game, 2. give the Player a reference to one if its cells, 3. Borrow a mutable reference to the Game, 4. Try to use the Player's cell reference, then the code fails to compile, even with Boxes. Does anyone know how to fix this?Irreclaimable
I've opened a new question here.Irreclaimable
@OliverEvans Good questions. The answer covers mutable vector because otherwise the OP's initial error could be easily fixed by passing &cells[0] instead of cells[0], which the OP documents as having tried, but gotten yelled at by the borrow checker. The OP didn't provide actual code, but the above strongly indicates that the problem comes from a mutable vector. However, you are completely right that the Box doesn't fix the problem and that the second example compiles without it. I've now switched the second example to Rc, which is the way to make such references work in safe Rust.Kantianism

© 2022 - 2024 — McMap. All rights reserved.