1
1

Rustの”ポインタ”は内部的には2個までらしい

Last updated at Posted at 2024-09-09

Rustを書く練習をしていた際に教えてもらいました。

疑問

ある日、Rustを書く練習をしていると、こんなエラーが出ました。

.stderr
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??
selfBox<Self>としてポインタの背後にいるのだからサイズはわかるのでは...?

サイズ不明の謎

謎だったのでDiscordで聞いてみると、こんな趣旨の返答をくれた方がいました。
曰く、「fat pointerextra 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です。

lib.rs
// ブログ記事風の構造体
// 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();
}

エラー(再掲):

.stderr
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を指定すれば良かったようです。ただ、これをやると別のエラーが出ます:

.stderr
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でないと怒られます。

よって、トレイトでデフォルト実装を提供しないと言う方法が一番早いと思います。ただ、記述量がトレイトを実装する型の数に比例して増えるため、原著のように型システムを使う方向に進みたくなります。

lib.rs
// ブログ記事風の構造体
// 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();
}
  1. 英語版です。dynキーワードが追加される前の古い記法を使っていますが、日本語版もあります
    コード:

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1