Smart pointers can crash your app Link to heading

Smart pointers are a feature of programming languages with manual memory management such as C++ and Rust. They are called “smart” because they provide automatic memory management, so the programmer doesn’t have to manually delete/free memory when the object in the heap is no longer used. However, these smart pointers are not infallible, and programmers still need to understand how they work in order to avoid fatal errors in their programs.

In C++, a typical smart pointer type is unique_ptr and shared_ptr, while in Rust, the closest data types are Box and Rc. They can be useful for storing dynamically allocated objects like linked lists, trees, or graphs. Smart pointers are introduced to reduce programmer’s burden of manual memory management, minimizing memory-related bugs.

Despite their usefulness, smart pointers have two major limitations that make them not so smart. First, they cannot resolve cycle references automatically, and the programmer must manually handle cycle references with weak pointers to avoid memory leaks. Second, their automatic memory management can crash a program in some cases if not manually handled.

These limitations do not exist in garbage-collected languages. A garbage collector can free up objects with cyclic references without issue and will certainly not crash a program. The crashing issue is more serious of the two and can be demonstrated with a simple program written in C++ or Rust.

Consider the C++ program main.cc:

#include <iostream>
#include <memory>

template<typename T>
struct Node {
  std::unique_ptr<Node<T>> next;
  T val;
  
  Node(T val, std::unique_ptr<Node<T>> next) : val{std::move(val)}, next{std::move(next)} {}
};

template<typename T>
struct LinkedList {
  std::unique_ptr<Node<T>> head;
  
  void push_front(T val) {
    auto node = std::make_unique<Node<T>>(std::move(val), std::move(head));
    head = std::move(node);
  }
};

int main(int argc, const char** argv) {
  auto n = std::stoi(argv[1]);
  LinkedList<int> list;
  for (int i = 0; i < n; ++i)
    list.push_front(i);
    
  return 0;
}

Then compile and run.

g++ -std=c++17 -O3 main.cc
./a.out 1000000
Segmentation fault (core dumped)

On my machine, I get segfault around 800000. I can assure you that the error does not come from linked list implementation itself, but rather from the smart pointer. Below is the Rust’s equivalent version.

struct Node<T> {
    val: T,
    next: Option<Box<Node<T>>>,
}

struct LinkedList<T> {
    head: Option<Box<Node<T>>>
}

impl<T> LinkedList<T> {
    fn new() -> Self {
        Self{ head: None }
    }
    
    fn push_front(&mut self, val: T) {
        let next = self.head.take();
        self.head = Some(Box::new(Node{val, next}));
    }
}

fn main() {
    let mut list = LinkedList::new();
    for i in 0..1000000 {
        list.push_front(i);
    }
}

You can also try this link on Rust Playground. Below is what I get.

thread 'main' has overflowed its stack
fatal runtime error: stack overflow
timeout: the monitored command dumped core
/playground/tools/entrypoint.sh: line 11:     8 Aborted                 timeout --signal=KILL ${timeout} "$@"

As you can see, it is quite easy to crash the program using smart pointers even with the correct usage. Obviously, if you implement the linked list in, say Java or C#, the program runs normally and would not crash. So, what is going on?

The program crashes because the default deallocation of the LinkedList’s smart pointer head causes recursive calls to its next node, which is not tail-recursive and can’t be optimized. If you are interested in more details, I recommend these references: 1, 2. The fix is to manually override a destructor method for the LinkedList data structure to free each node iteratively without recursion. In a sense, this defeats the purpose of the smart pointers — they fail to free the burden of manual memory management from the programmer.

In conclusion, smart pointers are a useful tool for managing memory in C++ and Rust, but they still require a fair amount of knowledge and manual handling in some cases. Otherwise, be prepared for an unexpected crash!

Technically, smart pointers are a type of garbage collection, i.e., reference counting garbage collection. In the story, garbage collection refers to tracing garbage collection.