Learn Rust With Entirely Too Many Linked Lists
39

Basics

Five operations: new, push_front, pop_front, push_back, pop_back. Each one is a sequence of pointer writes wrapped in unsafe. We allocate with Box::new and convert into raw with Box::into_raw; we free by going back through Box::from_raw. Symmetry between front and back lets us write half the code.

  1. push_front: allocate a node, leak the Box into a raw pointer, splice it ahead of the current front. The unsafe block wraps the dereferences. Three cases would be tempting (empty / single / general) but the symmetry of Option collapses them: if front was None then the back end is also empty and we set both.
    pub fn push_front(&mut self, elem: T) {
        unsafe {
            let new = NonNull::new_unchecked(Box::into_raw(Box::new(Node {
                front: None,
                back: self.front,
                elem,
            })));
            match self.front {
                Some(old) => (*old.as_ptr()).front = Some(new),
                None => self.back = Some(new),
            }
            self.front = Some(new);
            self.len += 1;
        }
    }
  2. pop_front: take the front pointer, reconstruct the Box<Node<T>> so its destructor will run, splice the new front in. Box::from_raw takes ownership of the heap allocation back from us; dropping the returned Box frees the node and runs its drop glue. We extract the element by value before the box drops.
    pub fn pop_front(&mut self) -> Option<T> {
        unsafe {
            self.front.map(|node| {
                let boxed = Box::from_raw(node.as_ptr());
                self.front = boxed.back;
                match self.front {
                    Some(new) => (*new.as_ptr()).front = None,
                    None => self.back = None,
                }
                self.len -= 1;
                boxed.elem
            })
        }
    }
  3. push_back and pop_back are the same code with front/back swapped throughout. In a real implementation you write them out explicitly — macros that generate them are a worse experience for everyone reading the code than four eight-line functions.
    pub fn push_back(&mut self, elem: T) {
        unsafe {
            let new = NonNull::new_unchecked(Box::into_raw(Box::new(Node {
                back: None,
                front: self.back,
                elem,
            })));
            match self.back {
                Some(old) => (*old.as_ptr()).back = Some(new),
                None => self.front = Some(new),
            }
            self.back = Some(new);
            self.len += 1;
        }
    }
    
    pub fn pop_back(&mut self) -> Option<T> {
        unsafe {
            self.back.map(|node| {
                let boxed = Box::from_raw(node.as_ptr());
                self.back = boxed.front;
                match self.back {
                    Some(new) => (*new.as_ptr()).back = None,
                    None => self.front = None,
                }
                self.len -= 1;
                boxed.elem
            })
        }
    }
  4. Drop is the iterative form we wrote in Chapter 1, modernized: while let Some(_) = self.pop_front() {}. Each pop_front reconstructs and drops one box, freeing one node. No recursion, constant stack.
    impl<T> Drop for LinkedList<T> {
        fn drop(&mut self) {
            while self.pop_front().is_some() {}
        }
    }