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 さん から助言をもらい理解の助けになりました。
ありがとうございました。