Learn Rust With Entirely Too Many Linked Lists
40

Panic Safety

Once T carries a destructor, every operation has to consider "what if a panic unwinds through the middle of me?" The bad outcome is leaving the list in a state where Drop will double-free, dangle, or skip a node. The standard tools: keep invariants per-step, or build on the side and swap atomically.

  1. The dangerous spot in Clone is T::clone(). We want to clone every element and link the new nodes into a fresh list. The risk: clone the first three elements, panic on the fourth, and an unwinding Drop frees three half-linked nodes. The fix: build an entirely new LinkedList<T> with push_back (which is itself panic-safe — only one mutation per call, in the right order) and only assign it to the destination at the very end.
    impl<T: Clone> Clone for LinkedList<T> {
        fn clone(&self) -> Self {
            let mut new = LinkedList::new();
            for item in self {
                // if T::clone panics here, `new` is dropped
                // cleanly by its own Drop impl. self is untouched.
                new.push_back(item.clone());
            }
            new
        }
    }
  2. Inside each individual operation, the invariant to maintain is: at every point where a panic could fire, the list's front/back/len and every node's front/back agree with each other. Allocate first (allocations can panic on OOM in some configurations), then do the rewiring as a sequence of writes that never leaves the structure half-linked.
    // good: allocate first, rewire after.
    let new = NonNull::new_unchecked(Box::into_raw(Box::new(node)));
    // from here down, no panics possible.
    self.front = Some(new);
    self.len += 1;
    
    // bad pattern: rewire while dropping old data, then alloc.
    // if the alloc panics, len and pointers disagree.
  3. Drop check meets panic safety: if a Node's T panics during drop, the whole list's Drop impl must still complete. Our Drop calls pop_front repeatedly; if one element's destructor panics, pop_front has already removed it from the list, so subsequent drops still find a consistent structure. The double-panic case (panicking while unwinding) aborts the process — which is the correct behavior.
    // Drop loop:
    // iter N:
    //   pop_front detaches node N from list (writes only)
    //   pop_front returns Some(elem)
    //   elem dropped at end of statement
    //     -- if elem's drop panics, N is already gone from list,
    //        list is still well-formed, unwind continues.