LoginSignup
6
3

More than 1 year has passed since last update.

Rustで子の構造体から親の構造体の値を安全に参照したい

Last updated at Posted at 2022-11-12

Pinの学習がてらに記事を書いてみることにしました。
理解の誤りなどありましたら、ご指摘のほどよろしくお願いいたします。

参考資料

RustのPinチョットワカル - OPTiM TECH BLOG
std::pin の勘所 - SlideShare

目次

1. はじめに
2. 何も考えずに実装してみる
3. Box
4. Box::pin
5. Pin化した型自体はムーブする?
6. クローンで置き換えてみる
7. 【脱線】安全なクローンを実装してみる
8. 【おまけ】もっと簡単かつ安全に子の構造体から親の構造体の値を参照したい
9. まとめ

1. はじめに

自己参照を持つ構造体を試行錯誤しながら実装していきます。
ポインタ、アドレス、スタック、ヒープ、スマートポインタ、クローン(ディープコピー)あたりの知識がなんとなくあれば読み進められるかと思います。
記事の内容自体はすごく簡単なものなので、気楽に読み進めていただければと思います。
今回使用したRustのバージョンは「rustc 1.65.0 (897e37553 2022-11-02)」になります。

2. 何も考えずに実装してみる

まずは、何も考えずに実装してみます、、、

Rust
struct Child {
    ptr: *const u32,
}

struct Parent {
    x: u32,
    c: Child,
}

impl Parent {
    pub fn new(x: u32) -> Self {
        let mut p = Parent {
            x,
            c: Child {
                ptr: std::ptr::null(),
            },
        };
        p.c.ptr = &p.x;
        print("new", &p);
        p
    }
}

fn main() {
    let mut p = Parent::new(1);
    print("main 1", &p);
    p.x = 2;
    print("main 2", &p);
    assert_eq!(&p.x as *const u32, p.c.ptr);
}

fn print(s: &str, p: &Parent) {
    println!(
        "{:<8} : &x = {:>014?} : ptr {:>014?} : x = {:>04?} : *ptr = {:>04?}",
        s,
        &p.x as *const u32,
        p.c.ptr,
        p.x,
        unsafe { *p.c.ptr }
    );
}
test
     Running `target\debug\pin.exe`
new      : &x = 0x00cb934ff790 : ptr 0x00cb934ff790 : x = 0001 : *ptr = 0001
main 1   : &x = 0x00cb934ff810 : ptr 0x00cb934ff790 : x = 0001 : *ptr = 0001
main 2   : &x = 0x00cb934ff810 : ptr 0x00cb934ff790 : x = 0002 : *ptr = 2134839296
thread 'main' panicked at 'assertion failed: `(left == right)`
  left: `0xcb934ff810`,
 right: `0xcb934ff790`', src\main.rs:29:5

new
newの中で ptr は x のアドレスをちゃんと指しているようです。
main 1:
newから返ってきたところで x がムーブしてアドレスが変わっていますが、ptr はnewから値を返す前の x のアドレスを指したままです。
main 2:
試しに x の値を2に変更してみることにします。期待としては *ptr の値も2になってほしいのですが、結果がとんでもない値になってしまいましたw

3. Box

x をヒープに置けばうまくいくんじゃないのかなということで、x の型ををBox<u32>に変更してみます。

Rust
struct Child {
    ptr: *const u32,
}

struct Parent {
    x: Box<u32>,
    c: Child,
}

impl Parent {
    pub fn new(x: u32) -> Self {
        let mut p = Parent {
            x: Box::new(x),
            c: Child {
                ptr: std::ptr::null(),
            },
        };
        p.c.ptr = &*p.x;
        print("new", &p);
        p
    }
}

fn main() {
    let mut p = Parent::new(1);
    print("main 1", &p);
    *p.x = 2;
    print("main 2", &p);
    assert_eq!(&*p.x as *const u32, p.c.ptr);
}
test
     Running `target\debug\pin.exe`
new      : &x = 0x01b60d063e90 : ptr 0x01b60d063e90 : x = 0001 : *ptr = 0001
main 1   : &x = 0x01b60d063e90 : ptr 0x01b60d063e90 : x = 0001 : *ptr = 0001
main 2   : &x = 0x01b60d063e90 : ptr 0x01b60d063e90 : x = 0002 : *ptr = 0002

newから返ってきても x のアドレスは変わらず、x の値を2に変更したら *ptr も2になりました。
これで問題はなくなったように思えますが、p.x を別の p.x で置き換えるとどうなるでしょうか?

Rust
fn main() {
    let mut p1 = Parent::new(1);
    let mut p2 = Parent::new(2);
    print("main 1", &p1);
    p1.x = p2.x;
    print("main 2", &p1);
    assert_eq!(&*p1.x as *const u32, p1.c.ptr);
}
test
     Running `target\debug\pin.exe`
new      : &x = 0x01ad7b545b70 : ptr 0x01ad7b545b70 : x = 0001 : *ptr = 0001
new      : &x = 0x01ad7b545b90 : ptr 0x01ad7b545b90 : x = 0002 : *ptr = 0002
main 1   : &x = 0x01ad7b545b70 : ptr 0x01ad7b545b70 : x = 0001 : *ptr = 0001
main 2   : &x = 0x01ad7b545b90 : ptr 0x01ad7b545b70 : x = 0002 : *ptr = 2069126064
thread 'main' panicked at 'assertion failed: `(left == right)`
  left: `0x1ad7b545b90`,
 right: `0x1ad7b545b70`', src\main.rs:30:5

&*p1.x が置き換え後の &*p2.x になりましたが、p1.c.ptr は置き換え前の &*p1.x を指したままです。

4. Box::pin

これまでのエラーを考えると、参照先の値が動いて消えてしまうことが原因のようです。
なので、参照先の値の移動(ムーブ)を阻止出来れば問題はなくなりそうです。
Rustには、値のムーブを阻止する機能としてPinが存在するので、Pinを使って解決を試みます。

Pinにも色々なやり方があるようですが、今回はBox::pinを使うことにしました。
それと、Pin化したい構造体にはPhantomPinnedを持たせる必要があります。
そうしないとPin化しても値のムーブは可能なままになってしまいます。

Rust
use std::marker::PhantomPinned;
use std::pin::Pin;

struct Child {
    ptr: *const u32,
}

struct Parent {
    x: Box<u32>,
    c: Child,
    _pinned: PhantomPinned,
}

impl Parent {
    pub fn new(x: u32) -> Pin<Box<Self>> {
        let p = Parent {
            x: Box::new(x),
            c: Child {
                ptr: std::ptr::null(),
            },
            _pinned: PhantomPinned,
        };
        let mut boxed = Box::pin(p);
        unsafe { boxed.as_mut().get_unchecked_mut().c.ptr = &*boxed.as_ref().x };
        boxed
    }
}

fn main() {
    let mut p1 = Parent::new(1);
    let mut p2 = Parent::new(2);
    print("main 1", &p1);
    p1.c.ptr = p2.c.ptr;
    p1.x = p2.x;
    p1 = p2;
    print("main 2", &p1);
    assert_eq!(&*p1.x as *const u32, p1.c.ptr);
}

コンパイル抜粋

compile
error[E0594]: cannot assign to data in dereference of `Pin<Box<Parent>>`
  --> src\main.rs:34:5
   |
34 |     p1.c.ptr = p2.c.ptr;
   |     ^^^^^^^^^^^^^^^^^^^ cannot assign
   |
   = help: trait `DerefMut` is required to modify through a dereference, but it is not implemented for `Pin<Box<Parent>>`

error[E0594]: cannot assign to data in dereference of `Pin<Box<Parent>>`
  --> src\main.rs:35:5
   |
35 |     p1.x = p2.x;
   |     ^^^^ cannot assign
   |
   = help: trait `DerefMut` is required to modify through a dereference, but it is not implemented for `Pin<Box<Parent>>`

error[E0507]: cannot move out of dereference of `Pin<Box<Parent>>`
  --> src\main.rs:35:12
   |
35 |     p1.x = p2.x;
   |            ^^^^ move occurs because value has type `Box<u32>`, which does not implement the `Copy` trait

Some errors have detailed explanations: E0507, E0594.
For more information about an error, try `rustc --explain E0507`.

Pin化した型のフィールドの値を置き換えようとするコードがエラーを吐くようになりました。うまくPin化できたようです。
しかし、p1 = P2 はエラーを吐いていません。これは、ちょっと意外な結果でした。

5. Pin化した型自体はムーブする?

では、p1 = P2 を試してみます。

Rust
fn main() {
    let mut p1 = Parent::new(1);
    let mut p2 = Parent::new(2);
    print("p1", &p1);
    print("p2", &p2);
    p1 = p2;
    print("p1", &p1);
    assert_eq!(&*p1.x as *const u32, p1.c.ptr);
}
test
     Running `target\debug\pin.exe`
p1       : &x = 0x01d73a363ec0 : ptr 0x01d73a363ec0 : x = 0001 : *ptr = 0001
p2       : &x = 0x01d73a3638b0 : ptr 0x01d73a3638b0 : x = 0002 : *ptr = 0002
p1       : &x = 0x01d73a3638b0 : ptr 0x01d73a3638b0 : x = 0002 : *ptr = 0002

P1 の内容が完全に p2 の内容になりました。Pin化した型自体のムーブは問題ないのでムーブしてもOKということなのでしょうか?

6. クローンで置き換えてみる

p1 = p2 が出来るのなら、p1 = p2.clone() も出来そうです。
少し脱線してきた気もしますが、続けます。

Rust
use std::marker::PhantomPinned;
use std::pin::Pin;

#[derive(Clone)]
struct Child {...

#[derive(Clone)]
struct Parent {...

impl Parent {...

fn main() {
    let mut p1 = Parent::new(1);
    let mut p2 = Parent::new(2);
    print("p1", &p1);
    print("p2", &p2);
    p1 = p2.clone();
    print("p1", &p1);
    assert_eq!(&*p1.x as *const u32, p1.c.ptr);
}
test
     Running `target\debug\pin.exe`
p1       : &x = 0x016339528a90 : ptr 0x016339528a90 : x = 0001 : *ptr = 0001
p2       : &x = 0x016339528ab0 : ptr 0x016339528ab0 : x = 0002 : *ptr = 0002
p1       : &x = 0x016339528ad0 : ptr 0x016339528ab0 : x = 0002 : *ptr = 0002
thread 'main' panicked at 'assertion failed: `(left == right)`
  left: `0x16339528ad0`,
 right: `0x16339528ab0`', src\main.rs:52:5

p2 のクローンにより、p1.x が新たなBoxに置き換わったようです。p1.ptr は &p2.x を指しています。
p1.ptr の参照先も今のところ存在していますが、p2 が置き換わってしまうとマズいことが起こりそうです。
そもそも p1.ptr は自分の親の値を指したいので、意図した結果になっていません。

7. 【脱線】安全なクローンを実装してみる

Pin化していても、正しい参照を持つ値を渡す(受け取る?)必要がありそうです。
なので、正しい参照を渡すようにクローンを実装してみます。
自己参照を持つような構造体にクローンを持たせるのは、精神衛生上よろしくないような気もしますが、脱線ついでにやってみます。

Rust
use std::marker::PhantomPinned;
use std::pin::Pin;

struct Child {...

struct Parent {...

impl Parent {...

impl Clone for Pin<Box<Parent>> {
    fn clone(&self) -> Pin<Box<Parent>> {
        Parent::new(*self.x)
    }
}

fn main() {
    let mut p1 = Parent::new(1);
    let mut p2 = Parent::new(2);
    print("p1", &p1);
    print("p2", &p2);
    p1 = p2.clone();
    print("p1", &p1);
    assert_eq!(&*p1.x as *const u32, p1.c.ptr);
}
test
     Running `target\debug\pin.exe`
p1       : &x = 0x01e986695b80 : ptr 0x01e986695b80 : x = 0001 : *ptr = 0001
p2       : &x = 0x01e986695ba0 : ptr 0x01e986695ba0 : x = 0002 : *ptr = 0002
p1       : &x = 0x01e986695bc0 : ptr 0x01e986695bc0 : x = 0002 : *ptr = 0002

見ての通りですが、Pin<Box<Parent>>に対してCloneを実装し、クローン元の x の値を引数にParent::newを返すようにしました。
これで p1.ptr の値も新たに生成されたBoxの方を指すようになりました。

8. 【おまけ】もっと簡単かつ安全に子の構造体から親の構造体の値を参照したい

今まで生ポインタを使って実装を試みてきましたが、生ポインタを使うのは思っていたよりかなり面倒だということが分かりました。
プログラマの精神衛生上もよろしくないので、ポインタ管理はプログラミング言語にまかせた方がよさそうです。

Rustだと、複数の場所から同じ値を参照したい場合に使うのはRc<RefCell<T>>(非同期ならArc<Mutex<T>>)になるのかなと思います。
Rc<RefCell<T>>と、その弱参照版であるWeak<RefCell<T>>を使用すれば同じようなことが出来そうです。

Rust
use std::borrow::Borrow;
use std::cell::RefCell;
use std::rc::{Rc, Weak};

#[derive(Clone)]
struct Child {
    ptr: Weak<RefCell<u32>>,
}

#[derive(Clone)]
struct Parent {
    c: Child,
    x: Rc<RefCell<u32>>,
}

impl Parent {
    pub fn new(x: u32) -> Self {
        let x = Rc::new(RefCell::new(x));
        let s = Parent {
            c: Child {
                ptr: Rc::downgrade(&x),
            },
            x: x.clone(),
        };
        s
    }
}

これでポインタおよびメモリ管理はRustがやってくれるようになりました。テストして確認してみます。

Rust
fn main() {
    let mut p1 = Parent::new(1);
    let mut p2 = Parent::new(2);
    print("p1", &p1);
    print("p2", &p2);
    p1 = p2.clone();
    print("clone", &p1);
    assert_eq!(
        p1.x.as_ref().as_ptr(),
        p1.c.ptr.upgrade().unwrap().as_ref().as_ptr()
    );
    p1 = p2;
    print("move", &p1);
    assert_eq!(
        p1.x.as_ref().as_ptr(),
        p1.c.ptr.upgrade().unwrap().as_ref().as_ptr()
    );
}
test1
     Running `target\debug\hello_cargo.exe`
p1       : &x = 0x01ab4a84be48 : ptr 0x01ab4a84be48 : x = 0001 : *ptr = 0001
p2       : &x = 0x01ab4a84bea8 : ptr 0x01ab4a84bea8 : x = 0002 : *ptr = 0002
clone    : &x = 0x01ab4a84bea8 : ptr 0x01ab4a84bea8 : x = 0002 : *ptr = 0002
move     : &x = 0x01ab4a84bea8 : ptr 0x01ab4a84bea8 : x = 0002 : *ptr = 0002

p1 を p2 のクローンで置き換えても。さらに p2 をムーブして置き換えても問題なし。

Rust
fn main() {
    let mut p1 = Parent::new(1);
    let mut p2 = Parent::new(2);
    println!("*p.ptr = {}", p1.c.ptr.upgrade().unwrap().as_ref().borrow());
    p1.x = p2.x;
    println!("*p.ptr = {}", p1.c.ptr.upgrade().unwrap().as_ref().borrow());
}
test1
     Running `target\debug\hello_cargo.exe`
*p.ptr = 1
thread 'main' panicked at 'called `Option::unwrap()` on a `None` value', src\main.rs:53:48

p1,p2をnewした後 p1.c.ptr の参照先の値をprintln!してみます。この時 p1.x は存在するので参照先の値の借用は成功しています。
次に p1.X の値を p2.x で置き換えたあと同じように、p1.c.ptr の参照先の値をprintln!してみます。p1.c.ptr の参照先である p1.x はムーブして消えてしまているので、参照先の値の借用は失敗しプログラムはパニックで終了しました。

最初の、何も考えずに実装したプログラムから、親の x の型をRc<RefCell<u32>>、子の ptr の型をWeak<RefCell<u32>> に変更しただけで、メモリ安全なプログラムになりました。素晴らしい!

もちろんこの後、Pin化して x のムーブを阻止するようにしたり、参照先の値の借用が失敗した場合の処理を追加してパニックで終了しないようなプログラムに変更することも可能です。

9. まとめ

Pinを学んだことで、学ぶ以前より生ポインタの危険性や、Rustのメモリ安全の考え方の理解が深まったような気がします。
最後までお付き合いいただきありがとうございました。

6
3
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
6
3