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::dropis never called - Memory leak ā The memory is never freed (by Rust)
- Safe function ā
forgetis 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
ManuallyDropwhen you need more control - Consider
std::mem::ManuallyDropfor 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::forgetprevents the destructor from running- It's safe! Leaking memory is not undefined behavior in Rust
- Use when transferring ownership to external code
ManuallyDropis 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_rawfor heap-allocated values - Use for implementing patterns like
into_raw()methods
