Skip to Content
Course content

39: Practice Exercise: Building a Generic Stack

Click on the "Edit" button in the top corner of the screen to edit your slide content.

When you first start building generic containers in Rust, it's easy to think that T is a magic wand that just "works" for everything. But the compiler is a lot more pedantic than that. I remember writing my first generic stack and getting absolutely hammered by the borrow checker and trait requirements because I assumed T inherited some basic capabilities.

Take a look at this snippet. It looks logically sound, but it won't even compile:

struct Stack<T> {
    elements: Vec<T>,
}

impl<T> Stack<T> {
    fn new() -> Self {
        Stack { elements: Vec::new() }
    }

    fn push(&mut self, item: T) {
        self.elements.push(item);
    }

    fn pop(&mut self) -> T {
        self.elements.pop().unwrap()
    }

    fn print_all(&self) {
        for item in &self.elements {
            println!("Item: {}", item);
        }
    }
}

The "T cannot be formatted" wall

If you try to compile this, the compiler will scream at you inside the print_all method. It'll tell you that T doesn't implement std::fmt::Display. This is a classic "aha!" moment. By saying Stack<T>, you're telling Rust: "This stack can hold absolutely anything."

The problem is that not "absolutely anything" can be printed to the console. A custom struct you wrote without a Display implementation, or a raw pointer, can't be used with println!. Rust won't let you write a method that might fail depending on what T ends up being. To fix this, we have to add a trait bound to the specific implementation block where the printing happens.

// We only implement print_all for types that actually support Display
impl<T: std::fmt::Display> Stack<T> {
    fn print_all(&self) {
        for item in &self.elements {
            println!("Item: {}", item);
        }
    }
}

Notice that I didn't put the bound on the struct definition itself. If I did, you'd never be able to create a Stack of something that isn't printable. By putting the bound on the impl block, the stack remains generic, but the print_all method only exists for types that satisfy the Display requirement. I find this is almost always the better architectural choice.

Replacing the panic with Option

Now look at the pop method. I used .unwrap(). In a quick prototype, that's fine. In a real project, it's a ticking time bomb. If you call pop() on an empty stack, your entire thread crashes. Since we're building a proper data structure, we should embrace Rust's Option type.

The Vec::pop method already returns an Option<T>. The simplest and most "Rustacean" thing to do is to just pass that Option right back to the caller. This shifts the responsibility to the user of the stack to decide how to handle the empty state, which is exactly where that decision belongs.

fn pop(&mut self) -> Option<T> {
    self.elements.pop()
}

With these two changes, we have a stack that is truly flexible, safe from crashes, and respects the trait system. You're no longer fighting the compiler; you're using it to define the contract of your data structure.




📋 Practical Task

Building a Generic Undo Buffer for String Edits

You are tasked with creating a system that tracks a history of strings (like a simple text editor's undo history). Instead of a basic stack, you need to build an UndoBuffer that manages a stack of strings but also tracks the total number of "actions" performed.

Requirements:

  • Create a generic struct UndoBuffer<T> that contains a Vec<T> and a usize counter called action_count.
  • Implement a push_action(&mut self, action: T) method that adds the item to the stack and increments the counter.
  • Implement an undo(&mut self) -> Option<T> method that removes the last action from the stack and decrements the counter (ensure the counter doesn't underflow).
  • Implement a get_history_size(&self) -> usize method that returns the current action_count.
  • Create a second impl<T: std::fmt::Debug> UndoBuffer<T> block with a method debug_print_history(&self) that prints the entire stack using the {:?} formatter.

Test your implementation by creating an UndoBuffer<String>, pushing three different strings, calling undo once, and then printing the remaining history and the action count.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.