無駄と文化

実用的ブログ

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

スマートバンドを買って以降まんまと健康志向になっている

2023年7月19日に『ポケモンスリープ』という睡眠ゲームアプリがリリースされました。
リリース前から話題になっていて、Twitter などでもゲーム画面のスクリーンショットなどを目にするようになりました。

それを見て、
以前から睡眠のログを取りたいと思っていたこともあってスマートバンド「Xiaomi Smart Band 7」を買うことにしました。

公式サイト ( https://www.mi.com/jp/product/xiaomi-smart-band-7/ ) から引用

 

なぜ「ポケモンスリープを始めました」ではないのかというと、定期的な充電や定期的なアプリ立ち上げができない私にポケモンスリープは無理だなと思ったからです。
一方、Xiaomi Smart Band 7 なら着けっぱなして睡眠ログをとっても7日間は充電がもちます。

そんなわけでスマートバンドを左手に着けた生活が始まったわけですが...

 

まんまと運動習慣が身についてしまった

ウォーキングやエクササイズが記録され、累計時間・消費カロリー・心拍数などの数値が目に見えることによって、まんまと運動習慣が身についてしまいました。

なんやかんやで運動習慣が継続している図

Xiaomi Smart Band 7 プリインストール時点で目標設定がされています。設定した目標をクリアすると「活力にあふれています!」というメッセージが出るので、目標クリアのために毎日せっせと運動を続けてしまっています。
デフォルトの目標設定は 30分間以上の運動・500kcal以上の消費・6000歩以上の歩行 です。

自分でも驚いているんですが、8月は累計で22時間以上の運動をして10,000kcal以上を消費しています。ひと月まるまるの結果ではないのですが、それでも10,000kcalを超えられたので嬉しいです。

9月も10,000kcalを超えられるようにがんばるぞ!

 

どんな運動をしているか

ウォーキング, スクワット, ズンバ, エアロビクス などをやっています。
空き時間・外の気温・その日の気分に応じてテキトーにやっている感じです。

が、ここ最近は朝起きて勤務を始めるまえに20分間ほどズンバをやるのが習慣になっていて、健康的すぎて自分でも怖いです。

 

ウォーキングは自宅の周辺で1周8kmのコースと1周5kmのコースを決めていて、時間と相談しながらそのどちらかをグルグルしています。

スクワット, ズンバ, エアロビクス は YouTube で再生しながらまねしてやっている感じです。
下記のような動画を見ています。

ズンバ, エアロビクス を実際にやってみて良かったところは、短時間でカロリーを消費すると同時に歩数も稼げることですね。
基本的にテンポよく足踏みしながら腕を振ったり足をあげたりする動きなので。

 

食事にも気を使うようになってしまった

そんなこんなで毎日の消費カロリーをロギングして追いかけていると、そのカロリーが入ってくる部分——つまりは食事 にも気を使うようになってしまいました。
まぁせっかくなら痩せようかなという気持ちもあって。

具体的な変化は、

  • 麺類を控えるようになった
  • もち麦ごはん・雑穀ごはんに親しむようになった
  • 炭水化物の代わりに豆腐を食べるようになった
  • サラダをもりもり食べるようになった
  • サバ缶を買いだめするようになった
  • 間食を控えるようになった
  • 禁酒した

といったところです。

糖質をいっさい摂らないというような極端なことはしていません。ただただ炭水化物をドカ食いしてお腹を膨らませるのはやめました。
炭水化物の代わりに豆腐やサラダをもりもり食べています。

 

普通の豆腐もおいしく食べているのですが、「とうふそうめん風」という商品がおいしくて感動しています。

紀文オンラインショップ ( https://www.kibun-shop.com/products/tofusomen/ ) から引用

豆腐でできたそうめん風の何かというやつですが "ニセそうめん" 的な違和感は無く、付属のつゆが美味しいので普通に楽しんで食べてます。

 

サラダはスーパーに売っている350g入りの袋サラダを1日1袋のペースで買っては食べ買っては食べとしています。

こんな感じのやつ。まな板も包丁も出さずに準備が済むので袋サラダは最高。

 

さらに、「サバ缶を1日1缶食べると痩せ体質になる」というのをどっかで見たのでサバ缶を買って食べるようになりました。
サバ缶おいしい、中骨がそのまま食べられるのが良い。

 

さらにさらに、禁酒するようになりました。お酒にもまぁまぁの量のカロリーがあるって聞いたので。
本来はお酒好きなんですが晩酌は控えつつ、これからは知人友人と食事にでかけたときにしこたま飲もうと思います。

 

そんで体重はどうなったのか?

こうなりました。

うーん、まだまだですね。痩せてぇ...。

 

まとめ

そういえば、
9月中に三井の体重推移データを API として公開しようと考えているのでお楽しみに。
バックエンドを Rust で書くか Go で書くか思案中です。

 

 

私からは以上です。