一件問題無さそうでもError code E0597が発生した件

2024/07/21に公開

前口上

trait Extract<'a> {
	fn extract(&'a self) -> &'a str;
}

struct Envelope<'a>(&'a str);

impl<'a> Extract<'a> for Envelope<'a> {
	fn extract(&'a self) -> &'a str {
		self.0
	}
}

fn f<'b>(arg: impl Extract<'b>) {
	let a = arg.extract();
}

fn main() {}

こんなコードをかくと、

error[E0597]: `arg` does not live long enough
  --> src/main.rs:14:10
   |
13 | fn f<'a>(arg: impl Extract<'a>) {
   |      --  --- binding `arg` declared here
   |      |
   |      lifetime `'a` defined here
14 |     let a = arg.extract();
   |             ^^^----------
   |             |
   |             borrowed value does not live long enough
   |             argument requires that `arg` is borrowed for `'a`
15 | }
   | - `arg` dropped here while still borrowed

このようなコンパイルエラーが発生した。

関数fは返却値を持つわけでもないのに、エラーが発生したのかどうにも理解できなかったが、ある程度解釈できたので、備忘録的にまとめておこうかなって

fの中身をバラしてみる

さて、fの実装をバラスト概ね以下のようになると思う。

fn f<'b>(arg: impl Extract<'b>) {
//	let a = arg.extract();
	let reference=&arg;
	let a=Extract::extract(reference);
}

ここで重要なのは、貰ってきたargは実体である反面、Extract::extractが要求しているのは&'a selfであるという点だ。

此処で今一度コンパイルしてみると、先とは少し違うコンパイルエラーを得ることが出来る

error[E0597]: `arg` does not live long enough
  --> src/main.rs:15:18
   |
13 | fn f<'b>(arg: impl Extract<'b>) {
   |      --  --- binding `arg` declared here
   |      |
   |      lifetime `'b` defined here
14 |     //    let a = arg.extract();
15 |     let reference = &arg;
   |                     ^^^^ borrowed value does not live long enough
16 |     let a = Extract::extract(reference);
   |             --------------------------- argument requires that `arg` is borrowed for `'b`
17 | }
   | - `arg` dropped here while still borrowed

これで問題が発生した場所と、コンパイラが咎めている部分を明確に分離することが出来た。

Extractをもう一度観察する

此処で、Extract traitをもう一度観察してみると、

trait Extract<'a> {
	fn extract(&'a self) -> &'a str;
}

このように、&selfは'aで注釈されており、返却値も当然のことながら'aで注釈されている。

この二つから解ること

さて、このふたつから何故コンパイルエラーが発生してしまうのか解釈してみようと思う。

let a=Extract::extract(reference);この時点でreferenceはExtract traitの'aでライフタイム注釈が為される。ここで、今一度fのシグネチャを観察すると、fn f<'b>(arg: impl Extract<'b>)のようになっており、この事実から、Extract traitの注釈'aは関数f'bと同じ意味を持つことになった。

従って、let reference = &arg;'bのライフタイムを持つ必要があるということになる。

しかし現実は、argは実体なので、関数fの終端でdropしてしまう。従って、let reference = &arg;で取得した参照は'bで注釈されたライフタイムを持つことが出来ずその結果コンパイルエラーが発生した。

このように解釈した。

解決方法

ここまで解ってしまえば解決方法はargにも同じライフタイム注釈を持たせりゃ良いと言うことは容易に理解できる。なので以下のようになるかなって

fn resolve<'b>(arg: &'b impl Extract<'b>) {
	let a = Extract::extract(arg);
	println!("{a}")
}

切口上

ライフタイム注釈の説明として、大体の場合、引数と返却値のライフタイムの整合性を主題に説明されていることが殆どであり、僕もそのような理解はある程度していたつもりではある。

しかしそのイメージが強すぎた故に今回のような戻り値が存在せず、取得した値を関数外にTransferしないのないのであればライフタイム絡みのコンパイルエラーは発生しないという思い込みがどこかにあり、このようなコンパイルエラーに直面したとき何故発生したのか混乱してしまった経緯がある。

また、個人的に実体をMoveすると言うことは、即ち参照を得ることに十全で優越するという変な思い込みも根底にあったのでなおさら混乱に拍車をかけてしまったと考えている。

今回腰を据えて何故コンパイルエラーが発生したのかを深く観察したことで、よりRustへの深い理解を得ることが出来た気がする。

追記 ~HRTBを使ってみた1例~

とりあえずコードを示す

//&selfのライフタイムと返却値の&strは別に同じライフタイムを持つ必要は無いので分離する。
trait Extract<'a, 'b> {
	fn extract(&'a self) -> &'b str;
}

//普通に実装
struct Envelope<'b>(&'b str);

//実装した結果は以下のようになる。
impl<'a, 'b> Extract<'a, 'b> for Envelope<'b> {
	fn extract(&'a self) -> &'b str {
		self.0
	}
}

//で、HRTBを使う場合、&selfのライフタイム注釈'aは修飾したtraitの関数内で閉じてるけど、
//返却値の&strのライフタイム注釈'bは外に出て行くので関数f側のライフタイム注釈として定義する
fn f<'b>(arg: impl for<'a> Extract<'a, 'b>) {
	let a = Extract::extract(&arg);
	println!("{a}")
}

//argを参照として与える場合は以下の通り。
//&argを注釈しているライフタイムがもっともな外来夫タイムを持つことを要請している
fn g<'a, 'b, 'c>(arg: &'a impl Extract<'b, 'c>)
where
	'a: 'b + 'c,
{
	let a = arg.extract();
	println!("{a}")
}

fn main() {
	let str = "hello world".to_string();
	let env = Envelope(&str);
	f(env);

	let env = Envelope(&str);
	g(&env);
}

また、別にselfを注釈しているライフタイムはtraitにくっついてる必要は無いのでよりシンプルに

trait Extract<'a> {
	fn extract<'s>(&'s self) -> &'a str;
}

struct Envelope<'a>(&'a str);

impl<'a> Extract<'a> for Envelope<'a> {
	fn extract<'s>(&'s self) -> &'a str {
		self.0
	}
}

fn f<'a>(arg: impl Extract<'a>) {
	let a = arg.extract();
	println!("{a}")
}

fn g<'a>(arg: &'a impl Extract<'a>) {
	let a = arg.extract();
	println!("{a}")
}

fn main() {
	let env = Envelope("hello");

	g(&env);
	f(env);

	let str = "hello".to_string();
	let env = Envelope(&str);

	g(&env);
	f(env);
}

こっちの方がよりシンプルなので良いかもしれない

Discussion