In this lesson, you will learn how to safely wrap low-level memory operations inside generic data structures by leveraging trait bounds to enforce invariant correctness.
When building generic containers (like a custom Vec<T>), you often reach a point where the compiler cannot guarantee memory safety, such as when manually allocating uninitialized memory. Because T is generic, you don't know if T requires drop glue, if it is safe to move, or if it satisfies specific memory requirements. This is where unsafe blocks become necessary, but they must be carefully constrained.
The Strategy: Trait-Bound Enforcement
To keep your unsafe code sound, you must demand that the type T fulfills specific criteria before allowing it into your container. If your unsafe block relies on the assumption that a type can be safely copied via ptr::copy, you should enforce this with a trait bound.
Consider this minimal example:
use std::ptr;
struct MyBox<T> {
ptr: *mut T,
}
impl<T> MyBox<T> {
pub fn new(value: T) -> Self {
// We use unsafe to allocate, but we wrap it in a safe API
let layout = std::alloc::Layout::new::<T>();
let ptr = unsafe { std::alloc::alloc(layout) as *mut T };
unsafe { ptr::write(ptr, value) };
MyBox { ptr }
}
pub fn take(self) -> T {
// We assume T is safe to read out of raw memory
unsafe { ptr::read(self.ptr) }
}
}
Encapsulation Rules
The key to writing this safely is twofold:
- Keep it Private: Never expose raw pointers or the internal
unsafememory layout to the user of your generic type. Theunsafelogic should be entirely contained within the implementation of your struct, while the public methods remain entirely safe. - Trait Bounds as Guards: If your
unsafeblock requiresTto beCopyorSend, add those bounds to yourimplblock. This prevents users from passing types that would violate the memory assumptions made inside yourunsafecode. By forcing the user to satisfy these bounds, you effectively move the "unsafe" burden from the container logic to the type signature, where the compiler can verify it.
Key Takeaway: Always encapsulate unsafe logic behind a safe, generic API and use trait bounds to explicitly define the invariants that T must satisfy to keep that memory operation sound.