37
Layout
The shape is what you'd write in C: a struct with front and back node pointers and a length. The Rust differences are the choice of pointer type (NonNull<Node<T>> instead of *mut Node<T>) and a zero-sized marker field that exists purely to inform the compiler about ownership.
-
NonNull<T>is*mut Tplus a guarantee that the bit pattern is never zero. Two consequences: the compiler can apply the null-pointer optimization soOption<NonNull<T>>is one pointer wide (theNoneis the all-zero bit pattern), and you opt into covariance overTinstead of the invariance you'd inherit from*mut T. We pay nothing at runtime for either.use std::marker::PhantomData; use std::ptr::NonNull; pub struct LinkedList<T> { front: Option<NonNull<Node<T>>>, back: Option<NonNull<Node<T>>>, len: usize, _marker: PhantomData<Box<Node<T>>>, } struct Node<T> { front: Option<NonNull<Node<T>>>, back: Option<NonNull<Node<T>>>, elem: T, } -
PhantomData<Box<Node<T>>>is a zero-sized field whose type encodes the lie we want the compiler to believe: this struct ownsNode<T>values on the heap. That gets us the right variance overT(covariant) and, crucially, the right dropck signal — the compiler will treat our list as if it owns and dropsNode<T>when the list is dropped, even though our actual fields are raw pointers.// these are zero bytes at runtime: use std::mem::size_of; assert_eq!(size_of::<PhantomData<Box<i32>>>(), 0); // the whole list is three pointer-sized words: assert_eq!( size_of::<LinkedList<i32>>(), 3 * size_of::<usize>(), ); - Why not
*mut Node<T>directly? Two reasons. (1)*mut Tis invariant inT, which we don't want for an owning collection —LinkedList<&'static str>should be usable whereLinkedList<&'a str>is expected. (2)Option<*mut T>is two words because*mut Tpermits a null bit pattern.NonNull<T>plusOptionplusPhantomDatais the canonical recipe.impl<T> LinkedList<T> { pub const fn new() -> Self { LinkedList { front: None, back: None, len: 0, _marker: PhantomData, } } }