Traits and Generics II and Smart Pointers

Overview

We’ll wrap up our discussion of traits/generics and discuss smart pointers in this lecture.

Trait Syntax in Functions and Composing Traits

use std::fmt;
// identity_fn, print_excited, print_sum
fn identity_fn<T>(x: T) -> T {x}

fn print_excited<T: fmt::Display>(x: T) {
    println!("{}!!!!!!!!!!!!!! :DDDDDDDDDD", x);
}

// An example of trait composition -- T must impl Display and PartialOrd
fn print_min<T: fmt::Display + PartialOrd>(x: T, y: T) {
    if x < y {
        println!("The minimum is {}", x);
    } else {
        println!("The minimum is {}", y)
    }
}

fn main() {
    println!("{:?}", identity_fn(Some(110)));
    print_excited(110);
    print_min(1, 10)
}

Here is the playground link

There are a couple of things about traits that we won’t have time to discuss in lecture: among them are trait objects, which you can read about here. You will see polymorphic traits in this week’s assignment (in particular, From<T>) and you can read about them in the CS242 notes – you don’t have to know too much about them for the purposees of the assignment. By now you should have the foundations to incorporate traits into your own code to enable good code reuse and design!

The Rc<T> Smart Pointer

Rc unlike Box allows you to have multiple pointers to the same chunk of heap memory. This happens through the magic of reference counting – every time to create a new pointer to this memory, the reference count is incremented. When one of these pointers is dropped, the reference count is decremented. Once the reference count is equal to 0, we deallocate the memory. Under the hood, this implementation (like the Box) implementation, needs to use unsafe code, but the interface as provided to you is safe. One thing that’s important to note is that you have an immutable reference to the heap memory so as to not violate Rust’s borrowing rules. We’ll see Rc in action in the following example:

use std::fmt;
use std::rc::Rc;
 
struct LinkedList {
    head: Option<Rc<Node>>,
    size: usize,
}

struct Node {
    value: u32,
    next: Option<Rc<Node>>,
}

impl Node {
    pub fn new(value: u32, next: Option<Rc<Node>>) -> Node {
        Node {value: value, next: next}
    }
}

impl LinkedList {
    pub fn new() -> LinkedList {
        LinkedList {head: None, size: 0}
    }
    
    pub fn get_size(&self) -> usize {
        self.size
    }
    
    pub fn is_empty(&self) -> bool {
        self.get_size() == 0
    }
    
    pub fn push_front(&self, value: u32) -> LinkedList {
        let new_node: Rc<Node> = Rc::new(Node::new(value, self.head.clone()));
        LinkedList {head: Some(new_node), size: self.size + 1}
    }
    
    // A tuple in Rust is like a struct -- you can access the zeroth element of 
    // a tuple name tup by writing "tup.0", you can access the element at index
    // 2 by writing "tup.2", etc.
    pub fn pop_front(&self) -> (Option<LinkedList>, Option<u32>) {
        match &self.head {
            Some(node) => {
                let val = node.value;
                let new_head: Option<Rc<Node>> = node.next.clone();
                let new_list = LinkedList {head: new_head, size: self.size - 1};
                (Some(new_list), Some(val))
            },
            None => (None, None)
        }
    }
}

impl fmt::Display for LinkedList {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let mut current: &Option<Rc<Node>> = &self.head;
        let mut result = String::new();
        loop {
            match current {
                Some(node) => {
                    result = format!("{} {}", result, node.value);
                    current = &node.next;
                },
                None => break,
            }
        }
        write!(f, "{}", result)
    }
}

// For sake of simplicity, we removed the impl Drop here. You can still override
// it, you just might have to use some Rust features that are currently unstable.
// If you really wanted it to be efficient, you might just use unsafe rust.

fn main() {
    let list: LinkedList = LinkedList::new();
    let version1 = list.push_front(10);
    let version2 = version1.push_front(5);
    let version3 = version2.push_front(3);
    let version4 = version2.push_front(4);
    let (version5, result) = version4.pop_front();
    println!("version1: {} has size {}", version1, version1.get_size());
    println!("version2: {} has size {}", version2, version2.get_size());
    println!("version3: {} has size {}", version3, version3.get_size());
    println!("version4: {} has size {}", version4, version4.get_size());
    println!("version5: {}, popped value: {}", version5.unwrap(), result.unwrap());
}

Here is the playground link

The RefCell<T> Smart Pointer

RefCell allows us to wrap a mutable piece of data behind an immutable reference. We can call borrow and borrow_mut on the RefCell to acquire references to the internal data – the borrowing rules are now enforced at runtime. Therefore, RefCell is safe, but it’s slightly more inefficient because it shifts what would have been a compile-time check to runtime. You commonly see Rc<RefCell<T>>, which lets us have a flexible handle to heap allocated data – just like Rc except now we can mutate things!