Rust中同一内存区可变引用与可变指针共存是否为UB?求递归结构处理方案
问题:递归Node结构的无死分支实现与UB问题
我有如下递归结构:
struct Node<T> { children: Vec<Node>, content: Option<T> }
我希望该结构不存在“死分支”(即多个节点指向content为None的情况)。为此,我在遍历结构时需要记录最后一个拥有多个子节点的节点。我原本的实现方式如下:
#[derive(Debug)] struct Node<T> { children: Vec<Node<T>>, content: Option<T> } impl <T> Node<T> { fn remove_leftmost(&mut self) { // I have `node`, a single mutable reference to my data let mut node: &mut Node<T> = self; let mut last_crossroads = None; while !node.children.is_empty() { let mut ptr = std::ptr::null_mut(); if node.children.len() > 1 { // Now I also have `ptr`, a mutable pointer to my data ptr = node; } // But then, I modify `node` so that it references another node node = &mut node.children[0]; if !ptr.is_null() { // `ptr` now refers to something that has no mutable reference to it // So `last_crossroads` should be ok, becoming the only mutable // reference to the precedent node last_crossroads = Some(unsafe { ptr.as_mut() }.unwrap()); } } if let Some(cross) = last_crossroads { cross.children.remove(0); } else { // Out of the scope of the question todo!(); } } } fn main() { let mut node = Node { children: vec![ Node { children: vec![ Node { children: vec![ Node { children: vec![], content: Some(4) } ], content: None } ], content: None }, Node { children: vec![], content: Some(2) } ], content: Some(1) }; node.remove_leftmost(); println!("{:?}", node); }
我知道持有同一内存区域的多个可变引用属于Undefined Behaviour(UB),但这种情况是否也适用于可变指针?如果不属于UB,这是否是正确的实现方式?
补充:根据Miri检测,该代码似乎存在UB,请问有什么可行的替代实现方案?
解答
关于可变指针与UB的问题
Rust的内存安全规则同样适用于原始指针:即使使用原始指针,同时存在多个可写的指针/引用指向同一内存区域,且其中至少一个被用于修改,就会触发UB。你的代码中,当把node(可变引用)转换为ptr(可变指针)后,node仍然是有效的可变引用,此时相当于同一内存区域同时存在一个可变引用和一个可变指针,违反了Rust的别名规则,这就是Miri检测出UB的原因。
替代实现方案
我们可以通过遍历过程中只保留当前路径的可变引用,同时记录父节点的分叉状态来避免UB,这里提供一种简洁且安全的实现方案:
#[derive(Debug)] struct Node<T> { children: Vec<Node<T>>, content: Option<T> } impl<T> Node<T> { fn remove_leftmost(&mut self) { let mut current = self; let mut last_crossroads: Option<&mut Node<T>> = None; loop { if current.children.len() > 1 { // 保存当前分叉节点的可变引用,此时之前的引用被覆盖,不存在别名冲突 last_crossroads = Some(current); } // 尝试获取第一个子节点的可变引用,没有则终止遍历 if let Some(first_child) = current.children.first_mut() { current = first_child; } else { break; } } if let Some(cross) = last_crossroads { cross.children.remove(0); } else { // 处理无分叉节点的场景 todo!(); } } }
这个方案的核心逻辑是:每次更新current为子节点的可变引用时,之前的current引用会被自动丢弃,确保同一时间只有一个可变引用指向树中的某个节点,完全符合Rust的安全规则,不会触发UB,同时也能准确记录最后一个分叉节点并完成左子节点的移除操作。
内容的提问来源于stack exchange,提问作者er1t
相关产品推荐
相关产品推荐

