Rust - strが引数/戻り値で使えない理由について
背景
Rustの公式ドキュメント 4.3スライス型にて、以下のようなシグニチャを持つ関数を定義するサンプルコードがある。
// 文字列を受け取って、その文字列中の最初の単語を返す関数
fn first_word(s: &String) -> &str
この時、戻り値の型として定義されているのは、 str
ではなく、 str
への参照である &str
だ。 usize
を返す場合は &usize
を使うのではなく、単に usize
をそのまま返すものとして定義されているし、実際そう実装できる。
この時、次のような疑問を抱いた。「usize
はそのまま参照ではなくusize
を返すことができるし、引数として受け取ることもできる。それと同じように、&str
ではなく str
を直接引数として受け取ったり戻り値で返したりすることはできるだろうか」。そこで、実際に str
を直接返すようにしてみたところ、以下のようなメッセージを出しながらコンパイルエラーとなった。
error[E0277]: the size for values of type `str` cannot be known at compilation time
--> src/main.rs:32:24
|
32 | fn test(s: &String) -> str {
| ^^^ doesn't have a size known at compile-time
|
= help: the trait `Sized` is not implemented for `str`
= note: the return type of a function must have a statically known size
試しに引数を str
にした場合も、同様のエラーとなる。
error[E0277]: the size for values of type `str` cannot be known at compilation time
--> src/main.rs:32:9
|
32 | fn test(s: str) -> String {
| ^ doesn't have a size known at compile-time
|
= help: the trait `Sized` is not implemented for `str`
= help: unsized locals are gated as an unstable feature
help: function arguments must have a statically known size, borrowed types always have a known size
疑問点
なぜ、 str
はそのまま関数の引数および戻り値として使えないのか?
&str
を使えば引数と戻り値どちらにも利用できるし、実際他のサンプルでも基本的に &str
を使っているようだが、ではこの &str
は何を指す参照で、なぜ参照にすれば引数と戻り値両方で利用できるのか?
"the size for values of type str
cannot be known at compilation time" というエラーメッセージが共通で出ているが、ここで言う "size" とは何で、それがコンパイル時とどう関わっているのか?
結論
関数の引数および戻り値はスタックで管理されるが、スタックの性質上、スタック上には固定長のデータしか乗せることができない。
一方、 str
は任意長のデータ型なので、それをそのまま関数の引数と戻り値に使うことはできない。
str
の代わりに、 str
への参照 &str
を使えば固定長のデータ型となるので、関数の引数および戻り値として使用できる。
エラーメッセージで "cannot be known at compilation time" と出ているのは、上記のようなメモリ管理上の問題をRustが事前に検知しているからである。
調査
まずはエラーメッセージ中の "the size for values of type str
cannot be known at compilation time" で検索してみたところ、stackoverflowで以下の質問と、その回答が見つかった。
What it means is harder to explain succinctly. Rust has a number of types that are unsized. The most prevalent ones are str and [T]. Contrast these types to how you normally see them used: &str or &[T]. You might even see them as Box<str> or Arc<[T]>. The commonality is that they are always used behind a reference of some kind.
Because these types don't have a size, they cannot be stored in a variable on the stack — the compiler wouldn't know how much stack space to reserve for them! That's the essence of the error message.
まずはエラーメッセージそのもの、「the size for values of type str cannot be known at compilation time」について
str を &'static str にすれば解決します。 Rustでは勝手にポインタが作られたりしないので str は任意長の文字列そのものを表わします。これを関数から返すということは、例えば1000文字の文字列ならば1000文字分のデータをコピーして呼出元に返すことになります。今のところRustは任意長のデータをスタックにコピーする操作は許していません。そこで &str とポインタを介すると16バイト(ポインタサイズ8バイト+strのサイズ8バイト)と定サイズになるのでエラーにならなくなります。
「str 型を生で使うことはない」と覚えてしまっていいでしょう。
これらの回答を見る限り、ポイントは以下の二点のようである。
これらの情報が手に入れば、以下のように説明が付きそうだ。
第一に、 str
は任意帳の文字列そのものを表す。すなわち、以下の構造で表されるような「文字のリスト」としての文字列そのものである。
[0] -> 'H'
[1] -> 'e'
[2] -> 'l'
[3] -> 'l'
[4] -> 'o'
関数の引数として渡された値はスタックへ乗せられる[3]。この時、既述の通り[1:1]、スタックへ載せられるデータは全て固定長、すなわち既知の固定サイズでなくてはならない。
一方、str
は任意長のデータ型であり固定長ではないため、 str
型の変数はスタックへは乗せられず(代わりに、ヒープに乗せなくてはならない)、そのため、 str
を引数のデータ型に使うことはできない。Rustはこうしたメモリ上の問題を事前に検出して、コンパイルエラーとしている。
関数の戻り値もやはりスタックへ乗せられる[4]。そのため、引数の時と同じ理由から、戻り値もやはり既知の固定サイズでなくてはならない。そのため、任意長のデータ型である str
は戻り値の型としても利用できず、コンパイルエラーとなる。
以上のことから、 str
を関数の引数および戻り値として扱うことはできない。しかし実際には、文字列を、より正確には文字列スライスを引数や戻り値に用いたいケースはあるし、実際、冒頭のRustドキュメントで提示されているのはそうした例である。
そこで、 str
ではなくその参照である &str
を用いることで、文字列(スライス)を引数および戻り値に利用できる。というのも、ポインタ型データのサイズは「ポインタのサイズ + ポインタが指すデータ型のサイズ」として事前に決定できるから、すなわち固定長だからである[5]。
ポインタ型は固定長のデータ型であるから、ポイント型の変数はスタックに乗せることができ、そのため、関数の引数および戻り値としても利用可能となる。事実、 str
に限らないオブジェクト(String
など)も同様に、オブジェクトそのものは非固定のデータ型であっても、 &String
のように参照を経由することで関数の引数や文字列として利用できている。
記事として整理する前のスクラップ
-
https://doc.rust-jp.rs/book-ja/ch08-02-strings.html
"Rustには、言語の核として1種類しか文字列型が存在しません。 文字列スライスのstrで、通常借用された形態&strで見かけます"
↩︎ ↩︎ -
https://doc.rust-jp.rs/book-ja/ch04-01-what-is-ownership.html
"スタックを高速にする特性は他にもあり、それはスタック上のデータは全て既知の固定サイズでなければならないということです"
↩︎ -
https://doc.rust-jp.rs/book-ja/ch04-01-what-is-ownership.html
"コードが関数を呼び出すと、関数に渡された値(ヒープのデータへのポインタも含まれる可能性あり)と、 関数のローカル変数がスタックに載ります。関数の実行が終了すると、それらの値はスタックから取り除かれます"
↩︎ -
https://brain.cc.kogakuin.ac.jp/~kanamaru/lecture/MP/final/part06/node9.html
"square 関数が終了するとスタック領域は図 7(c) のようになり、 戻り値 $v0 の値が変数 b に代入されてプログラムが終了する。"
↩︎ -
https://ja.stackoverflow.com/questions/65708/rustのresultについての質問-errore0277-the-size-for-values-of-type-str-cannot-be-kno
"そこで &str とポインタを介すると16バイト(ポインタサイズ8バイト+strのサイズ8バイト)と定サイズになるのでエラーにならなくなります。"
↩︎
Discussion