How do I work with mem::forget in Rust?

Walkthrough

mem::forget(value) takes ownership of a value and "forgets" about it without running its destructor. The value's memory is leaked, but this is sometimes necessary for FFI, transferring ownership, or implementing certain patterns safely.

Key concepts:

  • No destructor — The value's Drop::drop is never called
  • Memory leak — The memory is never freed (by Rust)
  • Safe function — forget is safe! Leaking is not unsafe in Rust
  • Ownership transfer — Useful when passing ownership to external code

When to use mem::forget:

  • Transferring ownership to FFI code
  • Preventing destructors from running
  • Implementing ManuallyDrop-like semantics inline
  • Working with affine types (use-once types)
  • Performance-critical code where you know cleanup isn't needed

Important considerations:

  • Memory leaks are safe in Rust (not undefined behavior)
  • Use ManuallyDrop when you need more control
  • Consider std::mem::ManuallyDrop for better ergonomics
  • Leaked resources are reclaimed when the process exits

Code Examples

Basic mem::forget Usage

use std::mem;
 
struct Resource {
    name: String,
}
 
impl Drop for Resource {
    fn drop(&mut self) {
        println!("Dropping resource: {}", self.name);
    }
}
 
fn main() {
    // Normal drop
    {
        let r = Resource { name: String::from("normal") };
        // Drop is called at end of scope
    }
    println!("---");
    
    // Forgotten - no drop
    {
        let r = Resource { name: String::from("forgotten") };
        mem::forget(r);
        // Drop is NOT called
        println!("After forget");
    }
    println!("End of scope");
}

Transferring Ownership to FFI

use std::mem;
use std::ffi::CString;
use std::os::raw::c_char;
 
// Simulated FFI function that takes ownership
extern "C" {
    fn free(ptr: *mut c_char);
}
 
fn transfer_to_ffi(s: String) {
    let c_string = CString::new(s).unwrap();
    let ptr = c_string.into_raw();
    
    // Transfer ownership to external code
    // Don't run CString's destructor - external code will free it
    mem::forget(c_string);
    
    // ptr is now owned by external code
    // In real FFI: external code will call free(ptr)
    
    // For this example, we'll just leak it
    println!("Transferred pointer: {:?}", ptr);
}
 
fn main() {
    transfer_to_ffi(String::from("Hello FFI"));
    println!("Transfer complete");
}

Preventing Destructor on Error

use std::mem;
use std::fs::File;
use std::io::{self, Write};
 
struct AtomicWriter {
    file: Option<File>,
    temp_path: String,
    final_path: String,
}
 
impl AtomicWriter {
    fn new(temp_path: &str, final_path: &str) -> io::Result<Self> {
        Ok(Self {
            file: Some(File::create(temp_path)?),
            temp_path: temp_path.to_string(),
            final_path: final_path.to_string(),
        })
    }
    
    fn write(&mut self, data: &[u8]) -> io::Result<()> {
        self.file.as_mut().unwrap().write_all(data)
    }
    
    fn commit(mut self) -> io::Result<()> {
        // Close the file first
        self.file = None;
        
        // Rename temp to final
        std::fs::rename(&self.temp_path, &self.final_path)?;
        
        // Success! Don't delete the temp file in drop
        mem::forget(self);
        
        Ok(())
    }
}
 
impl Drop for AtomicWriter {
    fn drop(&mut self) {
        // Only runs if commit wasn't called
        // Clean up the temp file
        let _ = std::fs::remove_file(&self.temp_path);
        println!("Cleaned up temp file: {}", self.temp_path);
    }
}
 
fn main() -> io::Result<()> {
    let mut writer = AtomicWriter::new("temp.txt", "final.txt")?;
    writer.write(b"Hello, World!")?;
    
    // Commit - temp file is NOT deleted
    writer.commit()?;
    
    println!("Commit successful");
    Ok(())
}

Consuming Self Without Drop

use std::mem;
 
struct Handle {
    id: u32,
    resource: *mut std::ffi::c_void,
}
 
impl Handle {
    fn new(id: u32) -> Self {
        Self {
            id,
            resource: std::ptr::null_mut(),
        }
    }
    
    // Transfer ownership to external code
    fn into_raw(self) -> *mut std::ffi::c_void {
        let ptr = self.resource;
        mem::forget(self);  // Don't run Drop
        ptr
    }
}
 
impl Drop for Handle {
    fn drop(&mut self) {
        println!("Dropping handle {}", self.id);
        // Would normally free self.resource
    }
}
 
fn main() {
    let handle = Handle::new(42);
    
    // Transfer ownership
    let ptr = handle.into_raw();
    println!("Transferred: {:?}", ptr);
    // handle's Drop was not called
}

Affine Types (Use-Once Types)

 
struct Session {
    token: String,
}
 
impl Session {
    fn new() -> Self {
        Self {
            token: String::from("secret_token"),
        }
    }
    
    // Must explicitly close - can't just drop
    fn close(self) {
        println!("Closing session with token: {}", self.token);
        // Normal cleanup
        // Drop will NOT run because we consume self
        mem::forget(self);
    }
    
    // Alternative: close with error
    fn close_with_error(self, reason: &str) {
        println!("Error closing session: {}", reason);
        mem::forget(self);
    }
}
 
impl Drop for Session {
    fn drop(&mut self) {
        panic!("Session must be explicitly closed with .close()");
    }
}
 
fn main() {
    let session = Session::new();
    session.close();
    // Safe - Drop was not called
    
    // This would panic:
    // let session2 = Session::new();
    // session2 dropped without calling close()
}

Avoiding Double-Free with mem::forget

use std::mem;
use std::ptr::NonNull;
 
struct OwnedBuffer {
    ptr: NonNull<u8>,
    len: usize,
}
 
impl OwnedBuffer {
    fn new(len: usize) -> Self {
        let layout = std::alloc::Layout::array::<u8>(len).unwrap();
        let ptr = unsafe { std::alloc::alloc(layout) };
        let ptr = NonNull::new(ptr).expect("allocation failed");
        Self { ptr, len }
    }
    
    fn as_slice(&self) -> &[u8] {
        unsafe { std::slice::from_raw_parts(self.ptr.as_ptr(), self.len) }
    }
    
    fn as_slice_mut(&mut self) -> &mut [u8] {
        unsafe { std::slice::from_raw_parts_mut(self.ptr.as_ptr(), self.len) }
    }
    
    // Transfer ownership - don't free on our side
    fn leak(self) -> (*mut u8, usize) {
        let (ptr, len) = (self.ptr.as_ptr(), self.len);
        mem::forget(self);
        (ptr, len)
    }
    
    // Reclaim a leaked buffer
    fn from_leaked(ptr: *mut u8, len: usize) -> Self {
        Self {
            ptr: unsafe { NonNull::new_unchecked(ptr) },
            len,
        }
    }
}
 
impl Drop for OwnedBuffer {
    fn drop(&mut self) {
        let layout = std::alloc::Layout::array::<u8>(self.len).unwrap();
        unsafe {
            std::alloc::dealloc(self.ptr.as_ptr(), layout);
        }
        println!("Freed buffer of {} bytes", self.len);
    }
}
 
fn main() {
    let buffer = OwnedBuffer::new(1024);
    
    // Transfer ownership somewhere else
    let (ptr, len) = buffer.leak();
    println!("Leaked buffer: ptr={:?}, len={}", ptr, len);
    
    // Can reclaim later
    let reclaimed = OwnedBuffer::from_leaked(ptr, len);
    println!("Reclaimed buffer of {} bytes", reclaimed.len);
    // reclaimed will be dropped properly
}

mem::forget vs ManuallyDrop

use std::mem::{self, ManuallyDrop};
 
struct Data {
    value: String,
}
 
impl Drop for Data {
    fn drop(&mut self) {
        println!("Dropping: {}", self.value);
    }
}
 
fn main() {
    // mem::forget - consumes the value
    let d1 = Data { value: String::from("forget") };
    mem::forget(d1);
    // d1 is gone, can't access it
    
    // ManuallyDrop - keeps the value accessible
    let mut d2 = ManuallyDrop::new(Data { value: String::from("manually_drop") });
    println!("Value: {}", d2.value);
    
    // Can still use it
    d2.value.push_str(" modified");
    println!("Modified: {}", d2.value);
    
    // Drop when ready
    unsafe {
        ManuallyDrop::drop(&mut d2);
    }
}

FFI Callback Registration

use std::mem;
use std::ffi::c_void;
 
// Simulated FFI callback system
static mut CALLBACK_DATA: *mut c_void = std::ptr::null_mut();
 
extern "C" fn simulate_register_callback(data: *mut c_void) {
    unsafe {
        CALLBACK_DATA = data;
    }
}
 
struct CallbackContext {
    name: String,
    data: Vec<u8>,
}
 
impl CallbackContext {
    fn new(name: &str) -> Self {
        Self {
            name: name.to_string(),
            data: Vec::new(),
        }
    }
    
    fn register(self) {
        let ptr = Box::into_raw(Box::new(self)) as *mut c_void;
        
        // Register with FFI - external code now owns the data
        simulate_register_callback(ptr);
        
        // Don't drop - FFI owns it now
        // (Box::into_raw already prevents drop, but showing the pattern)
        mem::forget(ptr);  // Actually not needed here - into_raw handles it
    }
    
    unsafe fn unregister() {
        if !CALLBACK_DATA.is_null() {
            let _box = Box::from_raw(CALLBACK_DATA as *mut Self);
            // Will be dropped properly now
            CALLBACK_DATA = std::ptr::null_mut();
        }
    }
}
 
impl Drop for CallbackContext {
    fn drop(&mut self) {
        println!("Dropping context: {}", self.name);
    }
}
 
fn main() {
    let ctx = CallbackContext::new("my_callback");
    ctx.register();
    
    println!("Callback registered");
    
    // Later, unregister to clean up
    unsafe {
        CallbackContext::unregister();
    }
}

Scoped Thread Pattern

use std::mem;
use std::thread;
 
struct Scope<'a> {
    // Would normally hold references to stack data
    _marker: std::marker::PhantomData<&'a ()>,
}
 
impl<'a> Scope<'a> {
    fn spawn<F>(&self, f: F)
    where
        F: FnOnce() + 'a,
    {
        // In a real implementation, this would spawn a thread
        // that must complete before 'a ends
        f();
    }
}
 
fn scope<'a, F, R>(f: F) -> R
where
    F: FnOnce(&Scope<'a>) -> R,
{
    let scope = Scope { _marker: std::marker::PhantomData };
    let result = f(&scope);
    
    // Ensure all scoped threads complete here
    // (simplified - real scoped_threadpool is more complex)
    
    result
}
 
fn main() {
    let data = vec![1, 2, 3, 4, 5];
    
    scope(|s| {
        s.spawn(|| {
            println!("Data: {:?}", data);
        });
    });
}

Forgetting with Leaked Memory Tracking

use std::mem;
use std::sync::atomic::{AtomicUsize, Ordering};
 
static LEAK_COUNT: AtomicUsize = AtomicUsize::new(0);
 
struct TrackedLeak<T> {
    value: T,
}
 
impl<T> TrackedLeak<T> {
    fn new(value: T) -> Self {
        Self { value }
    }
    
    fn leak(self) -> &'static T {
        LEAK_COUNT.fetch_add(1, Ordering::SeqCst);
        
        let ptr = Box::into_raw(Box::new(self.value));
        mem::forget(self);  // Don't drop self
        
        unsafe { &*ptr }
    }
}
 
impl<T> Drop for TrackedLeak<T> {
    fn drop(&mut self) {
        // Normal drop - no leak
        LEAK_COUNT.fetch_sub(1, Ordering::SeqCst);
    }
}
 
fn main() {
    let leaked1 = TrackedLeak::new(42i32).leak();
    let leaked2 = TrackedLeak::new(String::from("leaked")).leak();
    
    println!("Leaked int: {}", leaked1);
    println!("Leaked string: {}", leaked2);
    println!("Leak count: {}", LEAK_COUNT.load(Ordering::SeqCst));
}

When Not to Use mem::forget

use std::mem;
 
fn main() {
    // DON'T use forget when you want to explicitly drop
    // Use drop() instead:
    let s1 = String::from("hello");
    drop(s1);  // This runs the destructor
    
    // DON'T use forget when ManuallyDrop is more appropriate
    // If you need to access the value after "forgetting",
    // use ManuallyDrop:
    use std::mem::ManuallyDrop;
    let mut s2 = ManuallyDrop::new(String::from("world"));
    // Can still access
    println!("{}", *s2);
    unsafe { ManuallyDrop::drop(&mut s2); }
    
    // DON'T use forget for temporary optimization
    // Leaking memory is usually not the right solution
    // 
    // Instead, consider:
    // - Reusing allocations
    // - Using arenas
    // - Pooling resources
}

Summary

mem::forget Characteristics:

Property Description
Safety Safe (leaking is not UB)
Destructor Not called
Memory Leaked (never reclaimed)
Ownership Consumed

mem::forget vs Alternatives:

Approach Use Case Destructor
mem::forget(v) Transfer ownership, consume value Not called
ManuallyDrop<T> Need to access value after Manual control
drop(v) Explicitly run destructor Called
Box::into_raw(b) Transfer Box ownership Not called
std::mem::take() Replace with default Old value dropped

When to Use mem::forget:

Scenario Appropriate?
FFI ownership transfer āœ… Yes
Preventing drop on success path āœ… Yes
Implementing affine types āœ… Yes
into_raw() patterns āœ… Yes
Normal cleanup āŒ Use drop()
Need to access value later āŒ Use ManuallyDrop
Performance optimization āš ļø Rarely needed

Key Points:

  • mem::forget prevents the destructor from running
  • It's safe! Leaking memory is not undefined behavior in Rust
  • Use when transferring ownership to external code
  • ManuallyDrop is often more ergonomic for controlled destruction
  • The value is consumed and cannot be accessed after
  • Leaked memory is reclaimed when the process exits
  • Combine with Box::into_raw for heap-allocated values
  • Use for implementing patterns like into_raw() methods