ekicyou
@ekicyou

Are you sure you want to delete the question?

Leaving a resolved question undeleted may help others!

Rustで実体とその参照を同居させた構造体を作る方法

解決したいこと

Rustで実体とその参照を同居させた構造体を作りたい。
例)
文字列パーサを作成し、出力結果を構造体で引き回そうとしていますが、所有権で躓きました。unsafeをうまく使う必要があると思いますが具体策がわかりません。
解決方法を教えて下さい。

最小コード

pub struct Parsed<'a> {
    src: String,
    a: &'a str,
}

impl<'a> Parsed<'a> {
    fn new(src: String) -> Self {
        let a = &src;
        Self { src, a }
    }
}

最小例では単に"a = &src"としていますが、実際にはsrcを参照するパース結果を多数の部分文字列に格納しています。

発生している問題・エラー

   | impl Parsed {
   |      -- lifetime `'a` defined here
28 |     fn new(src: String) -> Self {
29 |         let a = &src;
   |                 ---- borrow of `src` occurs here
30 |         Self { src, a }
   |         -------^^^-----
   |         |      |
   |         |      move out of `src` occurs here
   |         returning this value requires that `src` is borrowed for `'a`

srcを借用している状態で構造体にmoveしているよ、と言われます。いわれていることは当然ですが、実際には構造体に格納し、後は変更可能にしていれば参照が壊れないことは保証可能です。

自分で試したこと

mem::transmuteまで使いましたが根本的なところでコンパイラを納得させることが出来ません。
1. 借用されていることを無かったことにする。
2. aのライフタイムを適切に設定する
3. その他
適切な宣言とunsafeを使うはずですが知識が足りず。
ご教授願います。

解決コード

解析結果を'staticにして、更にmem::transmuteを行うことで、src借用を解除することが出来ました。

fn main() {
    let src = "Hello, world!".to_owned();
    let parse = Parsed::new(src);
    println!("parsed");

    let thread = std::thread::spawn(move || {
        println!("thread start.");
        println!("  words len={}", parse.args.words().len());
        for w in parse.args.words() {
            println!("    word: {}", w);
        }
        println!("thread fin.");
    });

    thread.join().unwrap();
    println!("fin.");
}

pub struct Parsed {
    _src: String,
    pub args: ParseArgs<'static>,
}

impl<'a> Drop for Parsed {
    fn drop(&mut self) {
        println!("  Parsed::drop()");
    }
}

impl Parsed {
    pub fn new(src: String) -> Self {
        let args = ParseArgs::parse(&src);
        let args = unsafe { std::mem::transmute(args) };
        Self {
            _src: src,
            args: args,
        }
    }
}

pub struct ParseArgs<'a> {
    src: &'a str,
    words: Vec<&'a str>,
}

impl<'a> Drop for ParseArgs<'a> {
    fn drop(&mut self) {
        println!("  ParseArgs::drop()");
    }
}

impl<'a> ParseArgs<'a> {
    pub fn parse(src: &'a str) -> Self {
        let words = src.split(',').collect();
        Self {
            src: src,
            words: words,
        }
    }
    pub fn src(&self) -> &str {
        self.src
    }
    pub fn words(&self) -> &Vec<&str> {
        &self.words
    }
}

実行結果

parsed
thread start.
  words len=2
    word: Hello
    word:  world!
thread fin.
  Parsed::drop()
  ParseArgs::drop()
fin.

ライフタイム検証

ライフタイムを 'static に指定したとき、dropが消えてしまったらまずいなあと思って一応dropにログ出力してみました。ParseArgs::drop() が表示されていますので、普通に所有権消滅のタイミングで消え去ってくれるようですね。

1

1Answer

そもそもこのパターンならstrにくっついている'aというライフタイムはParsedのそれと一致するはずなので、ジェネリクスで任意のライフタイム'aを受け取っているのがそもそも変かなと思います。
この手の自己参照はRustでは表現できないものの一つとして有名です。回避策はいくつかあるとは思いますが、自身でこの手の構造を安全に表現する際によく使うのは「スライスが入る箇所にはスライスの範囲をもたせて、アクセサで実際のスライスを作り出す」方法です。

pub struct Parsed {
    src: String,
    a: Range<usize>,
}

impl Parsed {
    fn new(src: String) -> Self {
        let a = 0 .. src.len()
        Self { src, a }
    }
    fn a(&self) -> &str {
        &self.src[self.a.clone()]
    }
}

あるいは、unsafeでもいいのであればライフタイムの検証システムを'staticで騙すという方法もあるかと思います。この場合srcがmoveなどによって絶対に内部ポインタが変わらないという点をなんとかして保証する必要があるかなと思います(とはいえmutでないStringならまずポインタ値が変わることはないかなと思いますが)

pub struct Parsed {
    src: String,
    a: &'static str,
}

impl Parsed {
    fn new(src: String) -> Self {
        let a = unsafe { std::slice::from_raw_parts(src.as_ptr(), src.len()) };
        Self { a, src }
    }
    fn a(&self) -> &str {
        unsafe { std::mem::transmute(self.a) }
    }
}
2Like

Comments

  1. @ekicyou

    Questioner

    @Pctg-x8さん、回答ありがとうございます。
    おっっしゃる通り自己参照構造体をなんとか誤魔化して実現するのが
    目的です。
    Rangeを使う案が魅力的ですが、実際のパーサライブラリは入力&‘a strから出力&’a strを得るものなので‘staticで進めてみます。明日にでもサンプル検討します。
  2. @ekicyou

    Questioner

    2番目の例を参考に解決コードを作りました。
     1.'staticにすることでライフタイム追跡が途絶えること
     2.'staticに型変換するために std::mem::transmute すること
     3.コンパイル時のライフタイム追跡と所有権消滅時のdropは別物であること

    文字列実体と、その参照が同じタイミングでdropすることを保証できたので、
    文字列実体への操作を禁止することで正しく動作すると思ってます。

Your answer might help someone💌