5

I'm making a Tic-Tac-Toe game in Rust as a beginners project and was working on calculation the state of the game(won, draw, etc.) when I ran into this issue. I first wrote the following code:

fn status(&self) -> Status {
    if Board::has_won(self.x_board) {
        Status::Won(true)
    } else if Board::has_won(self.o_board) {
        Status::Won(false)
    } else if self.is_full() {
        Status::Draw
    } else {
        Status::None
    }
}

I then modified it to return a refrence:

fn status(&self) -> &Status {
    if Board::has_won(self.x_board) {
        &Status::Won(true)
    } else if Board::has_won(self.o_board) {
        &Status::Won(false)
    } else if self.is_full() {
        &Status::Draw
    } else {
        &Status::None
    }
}

I did this to link the lifetime of self and the returned value together so if the state of the board changed the reference to status could no longer be used.

I then tried this code:

fn status(&self) -> &Status {
    let status;
    if Board::has_won(self.x_board) {
        status = Status::Won(true)
    } else if Board::has_won(self.o_board) {
        status = Status::Won(false)
    } else if self.is_full() {
        status = Status::Draw
    } else {
        status = Status::None
    }

    &status
}

And it resulted in a compiler error saying it cannot return a referenced to owned data. Why doesn't the first snippet have this issue?

1 Answers1

4

My guess is that the first snippet is subject to lifetime extension, while the second is a reference to an explicit local variable and hence already has an explicit lifetime. The rules for lifetime extension are complex, so I'll let someone else discuss the specifics of that. Instead, I want to pose a slightly different design.

What you're doing is admirable. In fact, it's awesome. I'd never thought of tying a status variable like that to a data structure with references. That being said, you're lying to Rust a bit here, and we can get the behavior you want a bit more directly.

Basically, you have a Status. It's a value, it's not borrowed, and pretending that it's borrowed is just a bit awkward. It's not wrong, just awkward. But you want it to semantically behave like it's borrowed, even though it's not. We can use a PhantomData to do that.

Consider this. Leave your Status enum alone. Make it Copy and Clone, since it's nice and simple, and never take references to it. It can still be used sensibly as an independent data structure, provided you never assume that it's tied to any particular board.

Now, when you want to tie it to a particular board, use a new struct which I'll call BoardStatus.

struct BoardStatus<'a> {
  status: Status,
  _phantom: PhantomData<&'a Status>,
}

A BoardStatus really is just a Status. Its only non-zero-sized field is a Status, so the representations of the two should be identical. But it also has a PhantomData. PhantomData<T> is a zero-sized type (meaning it takes up no space at runtime) that pretends to contain a T for lifetime purposes. So BoardStatus is a Status (an owned status value, no borrows) that pretends to be borrowed for duration 'a. Then your status method can have this return type.

fn<'a> status(&'a self) -> BoardStatus<'a>

or, with lifetime elision

fn status(&self) -> BoardStatus<'_>

That way, there's a difference between the Status enum, which is independently testable and has no additional baggage; and the BoardStatus struct, which is still just a status but is explicitly tied to a board.

The best part is: this all has zero overhead. There's no actual pointer at runtime, so BoardStatus is exactly as efficient as a Status at runtime, with no indirection. A truly zero-cost abstraction.

Silvio Mayolo
  • 62,821
  • 6
  • 74
  • 116
  • Someone else said the first snippet may be due to the variants having the lifetime `'static`. Thanks for you suggestion of the `PhantomData`! – BlueZeeKing Feb 11 '23 at 05:07
  • 2
    I think that the first reference example works, not because of _lifetime extension_, but because of _[rvalue static promotion](https://github.com/rust-lang/rfcs/blob/master/text/1414-rvalue_static_promotion.md)_, similar phenomenon but not exactly the same. – rodrigo Feb 11 '23 at 13:59