Learn Rust With Entirely Too Many Linked Lists
34

Layout + Basics Redux

Rewrite the layout with raw pointers throughout: head and tail are both *mut Node<T>. Allocate via Box::new then immediately Box::into_raw; deallocate via Box::from_raw and let the box drop. There are no Box-owned references in flight, so there's nothing for stacked borrows to pop.

  1. New layout. Both fields are *mut Node<T>. Drop is now mandatory — without Box-owned children, the chain doesn't free itself.
    use std::ptr;
    
    pub struct List<T> {
        head: *mut Node<T>,
        tail: *mut Node<T>,
    }
    
    struct Node<T> {
        elem: T,
        next: *mut Node<T>,
    }
    
    impl<T> List<T> {
        pub fn new() -> Self {
            List { head: ptr::null_mut(), tail: ptr::null_mut() }
        }
    }
  2. push_back: heap-allocate via Box::new, hand the address to Box::into_raw to dissolve the Box, then wire it into the chain. No reference ever exists to the new node.
    pub fn push_back(&mut self, elem: T) {
        unsafe {
            let new_tail = Box::into_raw(Box::new(Node {
                elem,
                next: ptr::null_mut(),
            }));
    
            if self.tail.is_null() {
                self.head = new_tail;
            } else {
                (*self.tail).next = new_tail;
            }
            self.tail = new_tail;
        }
    }
  3. pop_front: reconstitute a Box from self.head via Box::from_raw, take its element, advance self.head, and let the Box drop. If we just popped the last node, null out self.tail.
    pub fn pop_front(&mut self) -> Option<T> {
        unsafe {
            if self.head.is_null() {
                None
            } else {
                let head_box: Box<Node<T>> = Box::from_raw(self.head);
                self.head = head_box.next;
                if self.head.is_null() {
                    self.tail = ptr::null_mut();
                }
                Some(head_box.elem)
            }
        }
    }
  4. Drop walks the chain by repeatedly popping. Each pop_front reconstitutes and drops one node; the loop terminates when head is null. For a list of T: !Copy non-trivially-destructible elements, Box<Node<T>>::drop runs the inner T's destructor too — drop_in_place semantics, paid for transparently by the box.
    impl<T> Drop for List<T> {
        fn drop(&mut self) {
            while self.pop_front().is_some() {}
        }
    }
  5. Run Miri. Stacked borrows: clean. Tree borrows: clean. The discipline is paying for itself: there is exactly one path to each node (the chain of raw pointers), and the only references that ever exist are short-lived ones inside method bodies that don't outlive their unsafe block. 2024 edition note: prefer &raw mut over &mut x as *mut _ if you ever need a one-off raw pointer to a local — it's the strict-provenance idiom Rust 2024 nudges you toward.
    $ cargo +nightly miri test
    running 4 tests
    test queue::tests::push_pop ... ok
    test queue::tests::peek      ... ok
    test queue::tests::iter      ... ok
    test queue::tests::drop_long ... ok
    
    test result: ok. 4 passed; 0 failed