®️

Rustのブロックの返り値の式で借用するとエラーになることがある(2024Editionで解決予定)

2025/02/09に公開

これは何の記事?

先日week in rustを読んでいたところ、The Rust 2024 Edition takes shapeに気になる記述がありました。

The project is also fixing the order in which temporary expressions at the end of a block get cleaned up. Rust blocks have return values, which lets if statements be used as expressions, among other cases. But when an expression is returned from a block, it might create temporary values. For example, this code creates a temporary value for cell.borrow():

fn f() -> usize {
let cell = RefCell::new("..");
// The borrow checker throws an error for the below
// line in the 2021 edition, but not the 2024 edition
cell.borrow().len()
}

どうやらこのコードが所有権のエラーを発生させるようです。
へぇ___

.
.
.

いやなんで?????

ぜんぜん意味ワカラナイので、もう少し原因を調べてみました。

(ちなみに、Rust 2024 でこの仕様は修正されてエラーが出なくなるそうです。)

まずエラーの原因調査します

件のコードを試しにplayground[1]で試してみると、確かにエラーが発生します

   Compiling playground v0.0.1 (/playground)
error[E0597]: `cell` does not live long enough
 --> src/lib.rs:5:3
  |
4 |   let cell = RefCell::new("..");
  |       ---- binding `cell` declared here
5 |   cell.borrow().len()
  |   ^^^^---------
  |   |
  |   borrowed value does not live long enough
  |   a temporary with access to the borrow is created here ...
6 | }
  | -
  | |
  | `cell` dropped here while still borrowed
  | ... and the borrow might be used here, when that temporary is dropped and runs the destructor for type `Ref<'_, &str>`
  |
  = note: the temporary is part of an expression at the end of a block;
          consider forcing this temporary to be dropped sooner, before the block's local variables are dropped
help: for example, you could save the expression's value in a new local variable `x` and then make `x` be the expression at the end of the block
  |
5 |   let x = cell.borrow().len(); x
  |   +++++++                    +++

For more information about this error, try `rustc --explain E0597`.
error: could not compile `playground` (lib) due to 1 previous error

エラーメッセージをみると

5 |   cell.borrow().len()
  |   ^^^^---------
  |   |
  |   borrowed value does not live long enough
  |   a temporary with access to the borrow is created here ...

borrowによってcellへの参照による一時変数が作られるのですが、参照先のcellが先にdropされてしまうことによるエラーの様です。

実際、その先のエラーを見るとcellはblockの終わりでdropされると書いてあります。

6 | }
  | -
  | |
  | `cell` dropped here while still borrowed

つまり、

  • 借用される変数(cell)はblockの終わりでdropされるが
  • block最後の式でborrowし生成した一時変数はblockが終わっても残る

ということになります。

だから、最後の行を

let x = cell.borrow().len(); x

に書き換えてくださいということですね

なるほどー、理解できました!

...
...

いやいや、よくわからん

cell.borrow().len()

borrow()で一時変数が作られること、これが参照元のcellより長生きしていることはわかりました。

え、なんで?

最後の式が返す値の内、利用したいのはlen()が返す値のみのはずです。そしてこれはusizeです。borrow()が作る一時変数がブロックの外まで生存する必要はないはずです。

なんでこんな仕様になってるのでしょうか?

もーちょっとだけ、原因調べてみました

他のドキュメントを漁ってみる

もう少し調べてみると、Rust Referenceにこんな記述がありました

https://doc.rust-lang.org/reference/destructors.html#temporary-scopes

The temporary scope of an expression is the scope that is used for the temporary variable that holds the result of that expression when used in a place context, unless it is promoted.

Apart from lifetime extension, the temporary scope of an expression is the smallest scope that contains the expression and is one of the following:

  • The entire function.
  • A statement.
  • The body of an if, while or loop expression.
  • The else block of an if expression.
  • The condition expression of an if or while expression, or a match guard.
  • The body expression for a match arm.
  • The second operand of a lazy boolean expression.

この記述によると、どうも A statement.が関係あるようです。

一時変数のスコープは基本的にその文(statement)になることが多いようで、例えば

let x = cell.borrow().len();

の場合、borrow() で発生した一時変数はこの文の間生存することになります。

最初のエラーになるコードの場合、

let x = fn();

のように、fn()の最後のブロックの式の返り値がxにバインドされる形になります。

つまり、

fn f() -> usize {
        let cell = RefCell::new("..");
        // The borrow checker throws an error for the below
        // line in the 2021 edition, but not the 2024 edition
        cell.borrow().len()
    }

の最後の式で生成した一時変数が、呼び出し元で使われる可能性があることを考慮しているため、ブロック外まで生存する様な仕様になっていたのでした。

繰り返しますが、Rust 2024 でこの仕様は修正されてエラーが出なくなるそうです。
なのでこの記事の内容は「へえ〜そんなこともあるんだー」程度にしてもらえればと思います。

最後に

もう2025年では…??

追記

Rust2024は2/20リリース予定なので、もうすぐ解決予定です。
https://github.com/rust-lang/rust-project-goals/issues/117

参考

脚注
  1. playgroundはこれ
    https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=ab8505c7cbcf4af68a09170fea207c4d ↩︎

Discussion