無駄と文化

実用的ブログ

Rust ではどんな値が &'static になれるのか

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 に置いています。

play.rust-lang.org

 

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