Rust では所有権と借用, そしてライフタイムという概念があるため、値そのものを取り扱うより参照 (借用) を取り扱うことが難しくなりがちです。
特に関数から参照を返そうとすると、そこにはライフタイムの概念が絡んできます。
例えば下記のように関数内から文字列スライスを返す場合、
fn f() -> &str { "str" }
このコードはこのままではコンパイルできません。
コンパイラはこのようなメッセージを出してきます、
missing lifetime specifier. expected named lifetime parameter. help: consider using the `'static` lifetime 意訳: ライフタイム指定がありません。ライフタイムパラメータが必要です。 help: `'static` ライフタイムを検討してください
実際、コンパイラの言うとおり 'static ライフタイムを明示すると先ほどのコードはコンパイル可能になります。
fn f() -> &'static str { "str" }
いったい何が起こっているのか、どんなシチュエーションでも 'static さえ付ければ万事解決なのか、そのへんを解説していきます。
'static ライフタイムとは何か
Rust のライフタイム注釈には特別な意味を持つキーワードがあります。
それが 'static で全てのライフタイムのなかで最も長いものを表します。つまりはプログラムが起動しているあいだずっとということです。
先ほどの例で -> &str ではコンパイルが通らず -> &'static str ならコンパイルが通るのは、ライフタイムを 'static とすることで「関数内の処理が終わっても値が drop されず有効であり続ける」ことを明示しているからでしょう。
ではどのような値が &'static になれるのか、実際にコードを書いて試してみましょう。
(最後まで読むのが面倒なひとのために) 3行まとめ
- スカラー型は
&'staticになれる - 複合型も
&'staticになれる - ヒープに置かれた値を参照している構造体は
&'staticになれない - 意図的にメモリをリークすることで
&'staticを得るテクニックがある
この記事で示したサンプルコードは Rust Playground に置いています。
スカラー型は &'static になれる
まずはスカラー型から。
Rust には6種類のスカラー型があります。
- 符号付き整数:
i8,i16,i32,i64,i128,isize - 符号無し整数:
u8,u16,u32,u64,u128,usize - 浮動小数点数:
f32,f64 - Unicode 文字:
char - 真理値:
true,false - ユニット型:
()
これらは全て &'static になれます。
サンプルコードを示すと、
fn i8() -> &'static i8 { &-1 } fn u8() -> &'static u8 { &42 } fn f64() -> &'static f64 { &std::f64::consts::E } fn char() -> &'static char { &'🦀' } fn bool() -> &'static bool { &true } fn unit() -> &'static () { &() } fn main() { println!("{:?}", i8()); // => -1 println!("{:?}", u8()); // => 42 println!("{:?}", f64()); // => 2.718281828459045 println!("{:?}", char()); // => '🦀' println!("{:?}", bool()); // => true println!("{:?}", unit()); // => () }
このように。
これらの型はどれも Sized で Copy でもあるので 'static ライフタイムとして取り扱えることはイメージに合うのではないでしょうか。
複合型も &'static になれる
タプル, スライス, 文字列スライス も (要素が &'static になれる型であれば) &'static になれます。
fn tuple() -> &'static (u8, char) { &(42, '🦀') } fn slice() -> &'static [char] { &['🦀'] } fn str() -> &'static str { "str" } fn main() { println!("{:?}", tuple()); // => (42, '🦀') println!("{:?}", slice()); // => ['🦀'] println!("{:?}", str()); // => "str" }
このことから分かるのは Sized ではない型でも &'static になれるということです。
Sized であることは必要条件ではないんですね。
返り値の型が &[T] や &str でも &'static になれない場合がある
最終的な返り値が &[T] や &str であっても、それがヒープに置かれた値の参照だと &'static になれないようです。
どういうことかと云うと、
// ↓↓↓ このようには書けない, &vec!['🦀'] のライフタイムは 'static ではない fn vec() -> &'static Vec<char> { &vec!['🦀'] // ^---------- // || // |temporary value created here // returns a reference to data owned by the current function } // ↓↓↓ このようには書けない, &String::from("str") のライフタイムは 'static ではない fn string() -> &'static String { &String::from("str") // ^------------------- // || // |temporary value created here // returns a reference to data owned by the current function }
上記の例はコンパイルが通りません。(エラーメッセージをコメントで併記しています)
vec!['🦀'] の型は Vec<char>, &vec!['🦀'] の型は &[char] です。
String::from("str") の型は String, &String::from("str") の型は &str です。
最終的に返されるのは &[char] や &str で、一つ前の例と同じです。関数のシグネチャも同じ。
ですがエラーが出てコンパイルできません。
Vec<T> や String の値を生成したとき、内部的にスライス (文字列スライス) が生成されてヒープに置かれます。
Vec<T> や String はヒープ上のスライスを指すポインタを所有していることになります。そして & によって参照を得ると、ヒープ上にあるスライスの参照が返るわけです。
これが (おそらく) エラーの原因で。ヒープ上に置かれた値の参照は &'static になれないようです。
-> &'static 付けとけば 'static になれるわけじゃないってことですね。
構造体が &'static になれる条件
構造体について考えていきましょう。
もっともシンプルでオーソドックスな例として Option<T> を取り上げます。一例として Option<u8> は&'static になれます。
fn some() -> &'static Option<u8> { &Some(42) } fn none() -> &'static Option<u8> { &None } fn main() { println!("{:?}", some()); // => Some(42) println!("{:?}", none()); // => None }
Option<T> のような組み込みの型だけでなく、ユーザーが定義した構造体でも同じように &'static になれます。
#[derive(Debug)] struct Foo<T>(T); fn struct() -> &'static Foo<u8> { &Foo(42) } fn main() { println!("{:?}", struct()); // => Foo(42) }
参照を内包している構造体も &'static になれる
Vec<T>, String の例を見て「参照を内包していると &'static になれないのかな?」と思いかけましたが、どうやらそうではないようです。
下記の例はコンパイルが通ります。
// &'static T を含む構造体 #[derive(Debug)] struct Bar<'a, T>(&'a T); // &'static な参照を内包する構造体 Bar の &'static な参照を返す fn struct_ref() -> &'static Bar<'static, u8> { &Bar(&42) } fn main() { println!("{:?}", struct_ref()); // => Bar(42) }
重要なのは参照している値が スタックに置かれているか/ヒープに置かれているか という点です。
ヒープに置いた値を内包している構造体は &'static になれない
試しにBox<T> を使ってヒープに置いた値を所有させるとコンパイルエラーを吐くようになります。
#[derive(Debug)] struct Baz<T>(Box<T>); // error[E0515]: cannot return reference to temporary value fn struct_box() -> &'static Baz<u8> { &Baz(Box::new(42)) // ^----------------- // || // |temporary value created here // returns a reference to data owned by the current function }
-> &'static 付けとけば 'static になれるわけじゃないってことですね。(2回目)
構造体が内包している型は ?Sized でも構わない
スライス &[T] や文字列スライス &str が &'static になれることからも分かるように、型が Sized である必要はありません。
?Sized な型を内包している構造体でも &'static になれます。
// &'static T を含む構造体 // T が Sized でなくてもいい #[derive(Debug)] struct Qux<'a, T: ?Sized>(&'a T); // &'static な参照を内包する構造体 Qux の &'static な参照を返す fn struct_ref_unsized() -> &'static Qux<'static, str> { &Qux("str is unsized") } fn main() { println!("{:?}", struct_ref_unsized()); // => Qux("str is unsized") }
意図的にメモリをリークすることで &'static を得る
ここからはおまけです。
Vec<T> から無理やり &'static [T] を得る
.leak() メソッドを使うことで Vec<T> から &[T] を取ることができ、そのようにして得たスライスは &'static になれます。
fn vec_leak() -> &'static [char] { // leak() メソッドを使って意図的にメモリリークさせると // Vec<char> から &'static [char] を取り出せる vec!['🦀'].leak() } fn main() { println!("{:?}", vec_leak()); // => ['🦀'] }
String から無理やり &'static str を得る
String にも .leak() メソッドがあります。それを使うと、
fn string_leak() -> &'static str { // leak() メソッドを使って意図的にメモリリークさせると // String から &'static str を取り出せる String::from("str").leak() } fn main() { println!("{:?}", string_leak()); // => "str" }
このように書けます。
メモリをリークすることさえも標準的な手段が用意されていて、ちゃんと型がついているのが面白いですね。
このあたりの思想や事情については「Rust 裏本」にある解説が興味深かったのでオススメです。
まとめ
どんな値が &'static になれるのか探っていきました。
体系的にまとまったレポートではないですが理解の助けになれば幸いです。
私からは以上です。
謝辞
.leak() メソッドを使ったテクニックについては @Toru31415926535 さん に教えてもらいました。
また X (旧: Twitter) で「ライフタイム何も分からん」と言ってるところに @nobkz さん から助言をもらい理解の助けになりました。
ありがとうございました。