在不安全的 Rust 中存储对结构内部数据的静态引用是否合法?

Chr*_*isB 6 unsafe lifetime language-lawyer rust

我有以下数据结构(简化):

use std::collections::HashMap;
pub struct StringCache {
    // the hashmap keys point to elements stored in `storage`
    // so never outlive this datastructure. the 'static refs are never handed out
    table: HashMap<&'static str, usize>,

    storage: Vec<Box<str>>,
}
Run Code Online (Sandbox Code Playgroud)

关于此处引用的生命周期向 Rust“撒谎”是合法/定义的行为吗?对我来说,这感觉就像违反了类型系统。这个数据结构的公共API健全吗?为了完整起见,以下是完整的实现:

use std::mem::transmute;
impl StringCache {
    pub fn intern(&mut self, entry: &str) -> usize {
        if let Some(key) = self.table.get(entry) {
            return *key;
        }
        let idx = self.storage.len();
        self.storage.push(entry.to_owned().into_boxed_str());
        // we cast our refs to 'static here.
        let key = unsafe { transmute::<&str, &'static str>(&self.storage[idx]) };
        self.table.insert(key, idx);
        idx
    }
    pub fn lookup(&self, idx: usize) -> &str {
        &self.storage[idx]
    }
}
Run Code Online (Sandbox Code Playgroud)

Cha*_*man 8

TL;DR:这很好。


这里存在三个潜在的问题:

  1. 创建寿命比实际寿命长的引用。
  2. 移动类型可能会使引用无效。
  3. 在删除数据时,我们可以使用删除后的字符串。

实际上它们都不是问题。让我们从第一个开始吧。

创建一个生命周期比其实际生命周期长的引用绝对没问题。许多野外代码都依赖它来正常工作,它没有在参考文献中的“行为被视为未定义”下列出(即使该列表并不详尽),Rust 中当前所有代码执行模型(例如 Stacked Borrows或MiniRust)采用了这一原则(事实上,Stacked Borrows 存在的真正原因是不依赖生命周期来实现稳健性,而是拥有更细粒度的模型),并且在UCG#231中指出,很明显,生命周期不会影响优化,只是当前未在某处指定。

那么我们就来讨论第二个问题。问题是移动StringCache(以及其storage)是否会使引用无效,因为它也会移动Vec的元素。或者使用更多技术术语,无论是移动Vec 重新标记(堆栈借用术语)其元素,还是断言其独特性。

你的直觉可能会说这很好,但现实要复杂得多。Vec用 定义其项目Unique,这意味着根据法律条文,移动它确实会使所有现有的指向元素的指针无效(这也是如此Box)。然而,许多野外代码都依赖于它是 false ,所以至少对于Vec(也许Box也是)我们可能希望它是 false。我认为依靠这一点是可以的。Miri也没有给予Vec任何特殊处理,据我所知,编译器也没有基于它进行优化。

对于 (3),您当前的定义(在table之前声明storage)显然没问题,因为它删除了第HashMap一个,但即使您以前的定义(storage首先声明)也很好,因为HashMap是用 声明的#[may_dangle],这意味着它承诺不会访问其元素(除了掉落它们之外)。这也是稳定性保证,因为它是可观察的,即使在与您的代码非常相似的代码中:

use std::collections::HashMap;

#[derive(Default)]
struct StringCache<'a> {
    storage: Vec<Box<str>>,
    table: HashMap<&'a str, usize>,
}

fn main() {
    let mut string_cache = StringCache::default();
    string_cache.storage.push("hello".into());
    string_cache.table.insert(&string_cache.storage[0], 0);
}
Run Code Online (Sandbox Code Playgroud)

该代码编译成功的事实是因为HashMap承诺在放置期间不会触及其元素。否则,我们可以有释放后使用。所以HashMap不能突然改变这个事实。


归档时间:

查看次数:

395 次

最近记录:

2 年,9 月 前