延迟边界检查时使用pointer::add是否安全?如何移除编译panic
test函数编译后的panic条件及相关疑问解答 示例代码
pub fn test(bytes: &[u8]) -> Option<u8> { let mut viewer = Viewer::new(bytes); if let Some(b'0') = viewer.try_peek() { viewer.skip(1); } viewer.try_peek() } pub struct Viewer<'a> { ptr: *const u8, end: *const u8, marker: std::marker::PhantomData<&'a [u8]>, } impl<'a> Viewer<'a> { #[inline] pub fn new(bytes: &'a [u8]) -> Self { let ptr = bytes.as_ptr(); let end = unsafe { ptr.add(bytes.len()) }; // if ptr > end { // unsafe { ::std::hint::unreachable_unchecked() }; // } Self { ptr, end, marker: std::marker::PhantomData, } } #[inline] pub fn try_peek(&self) -> Option<u8> { if self.ptr == self.end { // or >= return None; } Some(unsafe { *self.ptr }) } #[inline] pub fn skip(&mut self, n: usize) { //let new_ptr = unsafe { self.ptr.add(n) }; let new_ptr = self.ptr.wrapping_add(n); if new_ptr > self.end { panic!("out of bounds") } self.ptr = new_ptr; } }
一、移除panic条件的方案
要去掉skip里的panic,核心是保证指针偏移后不会越界,同时规避未定义行为(UB)。结合你的test函数场景(仅在try_peek成功时调用skip(1)),可以这样处理:
场景专属优化方案:
因为test里只有try_peek返回Some(_)时才调用skip(1),此时self.ptr < self.end是确定的,ptr.add(1)最多等于end(完全符合pointer::add的规则:结果指针可超出末尾一个字节),所以可以直接去掉panic判断,改用安全的add:#[inline] pub fn skip(&mut self, n: usize) { // 仅在调用方保证n不会导致指针越界时使用 self.ptr = unsafe { self.ptr.add(n) }; }这个方案能保留编译器的优化能力,且不会触发UB,但要求所有
skip的调用都遵守“偏移量合法”的前提。通用安全无panic方案:
如果要让skip支持任意合法偏移且无panic,可以用debug_assert!保留调试期检查,发布期自动消除:#[inline] pub fn skip(&mut self, n: usize) { let new_ptr = unsafe { self.ptr.add(n) }; // 调试期检查越界,发布期编译为空 debug_assert!(new_ptr <= self.end); self.ptr = new_ptr; }
二、pointer::add vs wrapping_add的选择与优化影响
- 文档推荐
wrapping_add的原因:当偏移量n不确定时,ptr.add(n)可能让指针超出原分配对象的合法范围(除末尾+1的位置),直接触发UB。wrapping_add会绕过边界检查,无论偏移多少都返回指针,但这会让编译器失去“指针在合法范围内”的优化依据,导致生成的代码更差。 - 在你的场景中,
skip(1)的调用前提是try_peek成功,此时ptr.add(1)的结果必然在合法范围内,所以用add完全安全,还能保留编译器的优化能力。
三、为何需要unreachable_unchecked或>=判断
unreachable_unchecked的作用:
在new函数中,ptr.add(bytes.len())理论上应该满足ptr <= end,但如果bytes.len()过大导致偏移量溢出isize范围,end可能会变成小于ptr的指针,破坏这个假设。unreachable_unchecked()是告诉编译器:ptr > end的情况永远不会发生,让编译器可以基于ptr <= end的假设做激进优化(比如简化try_peek的判断逻辑)。>=判断的必要性:
如果没有unreachable_unchecked(),编译器必须考虑ptr > end的极端情况,此时try_peek里的self.ptr == self.end判断就无法覆盖越界场景,需要改成>=才能正确返回None。但有了unreachable_unchecked()保证ptr <= end,==判断就足够了。
四、关于测试中的UB问题
当你用add替代wrapping_add且取消unreachable_unchecked注释时,程序看似正常工作,但实际上:
- 若
bytes.len()导致ptr.add(bytes.len())溢出isize,end会变成小于ptr的指针,后续try_peek的判断会出错,skip的add操作也会触发UB。 - UB是未定义行为,程序可能暂时正常,但随时可能崩溃、输出错误结果,或被编译器优化出逻辑混乱的代码,绝对不能依赖这种表象。
内容的提问来源于stack exchange,提问作者qRoC

