You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

为何`std::mem::forget`无法创建静态引用?代码崩溃解析

Rust代码段错误原因分析与修正

问题代码

pub trait Forget {
    fn forget_me_as_ptr(self) -> *const Self
    where
        Self: Sized,
    {
        let p = addr_of!(self);
        forget(self);
        p
    }

    fn forget_me_as_mut_ptr(self) -> *mut Self
    where
        Self: Sized,
    {
        self.forget_me_as_ptr() as *mut Self
    }

    fn forget_me_as_ref(self) -> &'static Self
    where
        Self: Sized,
    {
        self.forget_me_as_mut_ref()
    }

    fn forget_me_as_mut_ref(mut self) -> &'static mut Self
    where
        Self: Sized,
    {
        let p = addr_of_mut!(self);
        forget(self);
        unsafe { p.as_mut().unwrap() }
    }
}

pub fn default<T: Forget + Default>() -> &'static T {
    T::default().forget_me_as_ref()
}

#[derive(Debug)]
struct Player {
    health: u8,
    name: String,
    aliases: Vec<String>,
}

impl Drop for Player {
    fn drop(&mut self) {
        println!("Dropping [{}, {self:p}]", type_name::<Self>());
    }
}

impl Display for Player {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(
            f,
            "[{}, {self:p}] = name: {}, health: {}, aliases: {:#?}",
            type_name::<Self>(),
            self.name,
            self.health,
            self.aliases
        )
    }
}

impl Forget for Player {}

impl Default for Player {
    fn default() -> Self {
        Self {
            health: 100,
            name: "default".into(),
            aliases: vec!["default".to_string()],
        }
    }
}

fn main() {
    let p = default::<Player>();
    println!("{p}");
}

运行输出

$ cargo r
    Finished dev [unoptimized + debuginfo] target(s) in 0.00s
     Running `target/debug/static_factory`
Segmentation fault (core dumped)

问题分析

段错误直接原因

你拿到的指针指向的是栈上的临时对象。T::default()创建的Player实例分配在default函数的栈帧中,当forget_me_as_mut_ref执行完毕后,该函数栈帧会被销毁,栈上的内存会被回收或覆盖。虽然std::mem::forget跳过了Drop析构逻辑,但它无法阻止栈内存被系统回收——栈内存的生命周期完全由栈帧决定,函数返回后,对应内存就不再属于原对象。后续通过指针转成&'static引用访问这块内存,本质是访问已失效的栈内存,直接触发段错误。

逻辑漏洞

你误解了std::mem::forget的作用:

  • forget仅告知Rust不要执行对象的Drop方法,不会改变对象所在内存的生命周期。
  • 栈上的对象无论是否被forget,只要栈帧销毁,内存就会被回收;只有堆上分配的对象,在forget后才会脱离Rust自动内存管理,但前提是对象本身存储在堆上。
  • 直接将栈对象的指针转为'static引用完全错误,'static引用要求指向的内存从程序启动到结束始终有效,栈内存显然不满足这个条件。

引用、实例、指针的关系

  • 实例:数据的实际存储载体,分为栈实例和堆实例。栈实例存在于函数栈帧中,随栈帧销毁而消失;堆实例由Box、String、Vec等类型分配,内存由Rust所有权系统管理,当所有者被销毁且未被forget时,堆内存会被自动释放。
  • 指针:内存地址的裸值,Rust不对裸指针做任何生命周期或有效性检查,你可以用它指向任意内存,但访问时必须自行保证内存有效。
  • 引用:受Rust安全规则约束的指针,必须指向有效内存,且生命周期必须与被引用对象的生命周期匹配。&'static引用要求指向的内存全程有效,比如全局变量、用Box::leak泄漏到堆上的对象。

代码修正

要创建'static引用,正确做法是将对象分配到堆上,再通过泄漏堆内存让其脱离自动管理。可以直接用Rust标准库的Box::leak实现,无需自定义Forget trait:

use std::fmt;
use std::any::type_name;

pub fn default<T: Default>() -> &'static T {
    Box::leak(Box::new(T::default()))
}

#[derive(Debug)]
struct Player {
    health: u8,
    name: String,
    aliases: Vec<String>,
}

impl Drop for Player {
    fn drop(&mut self) {
        println!("Dropping [{}, {self:p}]", type_name::<Self>());
    }
}

impl fmt::Display for Player {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(
            f,
            "[{}, {self:p}] = name: {}, health: {}, aliases: {:#?}",
            type_name::<Self>(),
            self.name,
            self.health,
            self.aliases
        )
    }
}

impl Default for Player {
    fn default() -> Self {
        Self {
            health: 100,
            name: "default".into(),
            aliases: vec!["default".to_string()],
        }
    }
}

fn main() {
    let p = default::<Player>();
    println!("{p}");
}

修正说明

  • Box::new(T::default())将Player实例分配到堆上,Box作为堆对象的所有者。
  • Box::leak消耗Box并返回&'static mut T(可自动转为&'static T),同时泄漏堆内存——这块内存不会被Rust自动释放,会一直存在到程序结束,满足'static生命周期要求。
  • 修正后的代码运行时不会触发段错误,因为访问的是堆上的有效内存。

内容的提问来源于stack exchange,提问作者Self

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.16 02:33:14