Rustを書く練習をしていた際に教えてもらいました。
疑問
ある日、Rustを書く練習をしていると、こんなエラーが出ました。
error[E0277]: the size for values of type `Self` cannot be known at compilation time
--> src/lib.rs:8:9
|
8 | self
| ^^^^ doesn't have a size known at compile-time
|
= note: required for the cast from `Box<Self>` to `Box<(dyn State + 'static)>`
help: consider further restricting `Self`
|
7 | fn request_review(self: Box<Self>) -> Box<dyn State> where Self: Sized {
| +++++++++++++++++
なるほど...まてよ、doesn't have a size known at compile-time
??
self
はBox<Self>
としてポインタの背後にいるのだからサイズはわかるのでは...?
サイズ不明の謎
謎だったのでDiscordで聞いてみると、こんな趣旨の返答をくれた方がいました。
曰く、「fat pointer
はextra fat
にはなれないからキャストができずエラーが出ている」とのこと。
調べてみると、fat pointer
とは、データへのポインタと同時にそのメタデータも保持しているポインタのことらしいです。
例はスライスで、例えば[u8]
は(ポインタ、長さ)の二つを包含しています。
しかし、extra fat
とは...?(ポインタ、長さ)へのポインタを保持すればいくらでも太れるはず...
ポインタは2個まで?
これも聞いてみると、「Rustの"ポインタ"は1つか2つのポインタしか持てないから無理」とのことでした。
どうやら、今回のコードで使っていたトレイトオブジェクトには(データへのポインタ, vtableポインタ)
の二つが必要だったようで、そこに元のメタデータを加えるとポインタが3つになってしまうことがエラーの原因だったようです。
また、前述のポインタを保持するポインタはBox<Box<Self>>
のようにBox
を二重にかければ作れますが、また別の話だそう。
2024/9/15追記:
保持するポインタが1つのポインタは「痩せた」(thin)ポインタ、2つのポインタは「太った」(fat)ポインタと呼ばれるようです。
資料
実際のコードおよびエラー
元ネタはThe Rust Programming Languageの第17章3節1です。
// ブログ記事風の構造体
// the bookのお題がトレイトオブジェクトを使った状態管理なので
// stateフィールドを設けている
struct Post {
state: Box<dyn State>,
content: String,
}
// 状態管理用のトレイト
trait State {
fn request_review(self: Box<Self>) -> Box<dyn State> {
// ここがエラー:コンパイル時にサイズが不明
self
}
}
// 下書き「状態」
struct Draft {}
impl State for Draft {}
fn main() {
let mut p = Post {
state: Box::new(Draft {}),
content: String::new(),
};
p.state = p.state.request_review();
}
エラー(再掲):
error[E0277]: the size for values of type `Self` cannot be known at compilation time
--> src/main.rs:13:9
|
13 | self
| ^^^^ doesn't have a size known at compile-time
|
= note: required for the cast from `Box<Self>` to `Box<(dyn State + 'static)>`
help: consider further restricting `Self`
|
11 | fn request_review(self: Box<Self>) -> Box<dyn State> where Self: Sized {
| +++++++++++++++++
解決策
コンパイラが言う通りSelf: Sized + 'static
を指定すれば良かったようです。ただ、これをやると別のエラーが出ます:
error: the `request_review` method cannot be invoked on a trait object
--> src/main.rs:28:23
|
12 | where Self: Sized + 'static
| ----- this has a `Sized` requirement
...
28 | p.state = p.state.request_review();
| ^^^^^^^^^^^^^^
トレイトオブジェクトはSized
でないと怒られます。
よって、トレイトでデフォルト実装を提供しないと言う方法が一番早いと思います。ただ、記述量がトレイトを実装する型の数に比例して増えるため、原著のように型システムを使う方向に進みたくなります。
// ブログ記事風の構造体
// the bookのお題がトレイトオブジェクトを使った状態管理なので
// stateフィールドを設けている
struct Post {
state: Box<dyn State>,
content: String,
}
// 状態管理用のトレイト
trait State {
// デフォルト実装無し
fn request_review(self: Box<Self>) -> Box<dyn State>;
}
// 下書き「状態」
struct Draft {}
impl State for Draft {
fn request_review(self: Box<Self>) -> Box<dyn State> {
// こうするとエラーがなくなる
// おそらく型推論されてSizedだとわかるため
self
}
}
fn main() {
let mut p = Post {
state: Box::new(Draft {}),
content: String::new(),
};
p.state = p.state.request_review();
}