Rust impl optimizations

the things you skip when you “just write functions” — iterator traits, size hints, collection traits, lint attributes, and more.


1. what this is about

when you write a data structure in Rust, the minimum viable thing is:

impl<T> MyCollection<T> {
    pub fn new() -> Self { ... }
    pub fn push(&mut self, v: T) { ... }
    pub fn pop(&mut self) -> Option<T> { ... }
}

it works. but you’re leaving a lot on the floor — .collect() won’t work, for loops won’t work, == won’t work unless you derive it, clone() won’t work, the standard iterator adapters won’t chain onto your type. the sections below cover every common impl block you should consider adding, what it gives you, and when to skip it.


2. Iterator vs IntoIterator — the one people get wrong most

the wrong way: Iterator directly on your type

// BAD — Iterator on the collection itself
impl<T> Iterator for Stack<T> {
    type Item = T;
    fn next(&mut self) -> Option<T> { self.pop() }
}

this looks fine until you use any iterator adapter:

let mut s = Stack::from(vec![1, 2, 3]);
let sum: i32 = s.sum();          // looks innocent
println!("{:?}", s.peek());      // None — stack is gone. sum() drained it.

every method on the Iterator trait — .sum(), .count(), .map(), .filter(), .collect() — internally calls .next() repeatedly. if your collection is the iterator, any iterator method silently drains the data. there’s no separation between “the data” and “the thing consuming the data.”

the stdlib never does this. Vec, HashMap, BTreeSet — none implement Iterator directly.

the right way: a separate wrapper struct

// CORRECT

pub struct IntoIter<T>(Stack<T>);   // owns the stack, separate type

impl<T> Iterator for IntoIter<T> {  // Iterator on the WRAPPER
    type Item = T;
    fn next(&mut self) -> Option<T> { self.0.pop() }
}

impl<T> IntoIterator for Stack<T> {
    type Item = T;
    type IntoIter = IntoIter<T>;
    fn into_iter(self) -> IntoIter<T> { IntoIter(self) }
}

now the stack and the iterator are two different types. the draining is explicit and intentional:

let s = Stack::from(vec![1, 2, 3]);
let sum: i32 = s.into_iter().sum();  // s is moved into IntoIter — intent is clear

let s = Stack::from(vec![1, 2, 3]);
let top = s.peek();  // fine, s is still a Stack, not an iterator

IntoIterator also enables for loops

once you implement IntoIterator, for loops just work:

let s = Stack::from(vec![1, 2, 3]);
for item in s {
    println!("{}", item);  // prints 3, 2, 1
}

Rust desugars for x in collection into collection.into_iter() then repeated .next() calls.

borrowing iterators: iter() and iter_mut()

IntoIterator consumes the collection. for non-destructive iteration, implement borrowing iterators as methods (by convention, not a trait):

pub fn iter(&self) -> std::slice::Iter<'_, T> {
    self.data.iter()       // yields &T, doesn't move anything
}

pub fn iter_mut(&mut self) -> std::slice::IterMut<'_, T> {
    self.data.iter_mut()   // yields &mut T
}

and the corresponding IntoIterator impls for &Stack<T> and &mut Stack<T>:

impl<'a, T> IntoIterator for &'a Stack<T> {
    type Item = &'a T;
    type IntoIter = std::slice::Iter<'a, T>;
    fn into_iter(self) -> Self::IntoIter { self.data.iter() }
}

impl<'a, T> IntoIterator for &'a mut Stack<T> {
    type Item = &'a mut T;
    type IntoIter = std::slice::IterMut<'a, T>;
    fn into_iter(self) -> Self::IntoIter { self.data.iter_mut() }
}

with these, you can borrow-iterate without consuming:

let s = Stack::from(vec![1, 2, 3]);
for x in &s { println!("{}", x); }  // s still alive after loop

3. size_hint and ExactSizeIterator

size_hint — tell the iterator how many items remain

size_hint is a method on Iterator that returns (lower_bound, Option<upper_bound>). by default it returns (0, None) — “no idea.” callers may use this to pre-allocate:

fn size_hint(&self) -> (usize, Option<usize>) {
    let len = self.0.len();
    (len, Some(len))   // exact — we know exactly how many are left
}

this alone unlocks a perf improvement in .collect():

// with size_hint, Vec::from_iter pre-allocates the right capacity upfront
let v: Vec<i32> = stack.into_iter().collect();  // 0 reallocations

without it, the vec grows dynamically and may reallocate 2-3 times.

ExactSizeIterator — guarantee the hint is exact

ExactSizeIterator is a marker trait that says “my size_hint is always exact.” it requires no new methods — just the declaration. in return you get .len() on the iterator for free:

impl<T> ExactSizeIterator for IntoIter<T> {}  // no body needed

now:

let s = Stack::from(vec![1, 2, 3]);
let mut iter = s.into_iter();
assert_eq!(iter.len(), 3);
iter.next();
assert_eq!(iter.len(), 2);   // tracks correctly

size_hint alone is just a hint — callers may ignore it. ExactSizeIterator is the contract that it’s correct. also enables compile-time checks in some generic code that bounds on ExactSizeIterator.

DoubleEndedIterator — iterate from both ends

if your underlying data supports it (a vec does), implement this to get .rev() for free:

impl<T> DoubleEndedIterator for IntoIter<T> {
    fn next_back(&mut self) -> Option<T> {
        self.0.data.first().cloned()  // or however you want to define "back"
    }
}

with this:

let s = Stack::from(vec![1, 2, 3]);
let reversed: Vec<i32> = s.into_iter().rev().collect();
// also enables .rfind(), .rfold(), .rposition()

4. FromIterator and Extend — the collect side

FromIterator — powers .collect()

FromIterator<T> is the trait that powers .collect(). without it, .collect::<YourType>() is a compile error.

// without FromIterator:
let s: Stack<i32> = (1..=5).collect();
// error: the trait `FromIterator<i32>` is not implemented for `Stack<i32>`

implementation is usually a one-liner:

impl<T> FromIterator<T> for Stack<T> {
    fn from_iter<I: IntoIterator<Item = T>>(iter: I) -> Self {
        Self { data: Vec::from_iter(iter) }
    }
}

now you can:

let s: Stack<i32> = (1..=5).collect();

let s: Stack<i32> = vec![1, 2, 3]
    .into_iter()
    .filter(|x| x % 2 == 0)
    .collect();

// round-trip: consume, transform, collect back into your type
let doubled: Stack<i32> = original.into_iter().map(|x| x * 2).collect();

Extend — bulk-push from any iterator

Extend<T> lets you push multiple items from an iterator into an existing collection, without consuming it:

impl<T> Extend<T> for Stack<T> {
    fn extend<I: IntoIterator<Item = T>>(&mut self, iter: I) {
        self.data.extend(iter);
    }
}

use case:

let mut s = Stack::new();
s.extend([1, 2, 3]);
s.extend(other_stack.into_iter().filter(|x| x > 0));

also enables += style patterns in some generic code that bounds on Extend.


5. conversion traits — From, Into, TryFrom

From<Vec<T>> — construct from a vec

impl<T> From<Vec<T>> for Stack<T> {
    fn from(data: Vec<T>) -> Self { Self { data } }
}

From is reflexive: implementing From<A> for B automatically gives you Into<B> for A via a blanket impl. so you get both directions for free:

let v = vec![1, 2, 3];
let s = Stack::from(v);          // From
let s: Stack<i32> = v.into();    // Into — free, no extra impl needed

From<Stack<T>> for Vec<T> — convert out

implement the reverse direction too:

impl<T> From<Stack<T>> for Vec<T> {
    fn from(s: Stack<T>) -> Vec<T> { s.data }
}

now consumers can escape to a vec without manually draining:

let v: Vec<i32> = my_stack.into();   // or Vec::from(my_stack)

when to add TryFrom

use TryFrom when construction can fail — e.g. a bounded stack:

impl<T> TryFrom<Vec<T>> for BoundedStack<T> {
    type Error = CapacityError;
    fn try_from(data: Vec<T>) -> Result<Self, Self::Error> {
        if data.len() > MAX { Err(CapacityError) } else { Ok(Self { data }) }
    }
}

6. derivable traits — always think about these first

before writing any impl by hand, check if #[derive] handles it:

#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
pub struct Stack<T> { data: Vec<T> }
trait what it gives you derive condition
Debug {:?} formatting all fields implement Debug
Clone .clone() all fields implement Clone
PartialEq == and != all fields implement PartialEq
Eq marker for exact equality PartialEq must be derived/implemented first
Hash use in HashMap / HashSet all fields implement Hash; only if Eq too
PartialOrd <, >, <=, >= all fields implement PartialOrd
Ord .sort(), BTreeMap keys PartialOrd + Eq first
Default Stack::default() → empty all fields implement Default

[!warning] Hash and Eq must agree: if two values are ==, their hashes must be identical. if you implement PartialEq manually (e.g. custom equality logic), you must also implement Hash manually. never mix derived Eq with manual Hash or vice versa.

[!note] Ord requires a total ordering. if your type has f32 or f64 fields, you can’t derive Ord or even Eq because floats have NaN. use PartialOrd only, or wrap floats in an ordered newtype.

implementing Default manually when the derived version won’t do

// derive works fine if all fields have Default
#[derive(Default)]

// manual when you need custom logic
impl<T> Default for Stack<T> {
    fn default() -> Self { Self { data: Vec::with_capacity(16) } }  // pre-allocate
}

7. Display and Debug — formatting

Debug should always be derived unless your type has sensitive fields. Display is for human-readable output — implement it manually:

use std::fmt;

impl<T: fmt::Display> fmt::Display for Stack<T> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "[")?;
        for (i, item) in self.data.iter().enumerate() {
            if i > 0 { write!(f, ", ")?; }
            write!(f, "{}", item)?;
        }
        write!(f, "] (top →)")
    }
}

use case:

println!("{}", my_stack);   // Display
println!("{:?}", my_stack); // Debug
println!("{:#?}", my_stack); // pretty Debug

8. #[must_use] — lint for ignored return values

#[must_use] on a function makes the compiler warn if the return value is discarded. use it on purely observational methods:

#[must_use]
pub fn peek(&self) -> Option<&T> { self.data.last() }

#[must_use]
pub fn len(&self) -> usize { self.data.len() }

#[must_use]
pub fn is_empty(&self) -> bool { self.data.is_empty() }

do not put it on methods where ignoring the return value is a valid use case:

// ❌ wrong — pop() is legitimately called for its side effect (discard top)
#[must_use]
pub fn pop(&mut self) -> Option<T> { ... }

s.pop();  // valid "discard top" usage, but triggers a warning

#[must_use] on a type applies to any function returning that type:

#[must_use = "iterators are lazy — use them or you get nothing"]
pub struct IntoIter<T>(Stack<T>);

9. [lints] in Cargo.toml — crate-level lint config

rather than scattering #![deny(...)] inside source files, configure lints in Cargo.toml:

[lints.rust]
unsafe_code    = "forbid"
unused_imports = "warn"
dead_code      = "warn"

[lints.clippy]
all            = "warn"
pedantic       = "warn"
unwrap_used    = "deny"    # forces you to handle None/Err explicitly
expect_used    = "warn"
missing_docs_in_private_items = "warn"

clippy lints relevant specifically to the patterns above:

lint what it catches
clippy::must_use_candidate functions that should probably have #[must_use]
clippy::missing_trait_impls suggests missing common impls
clippy::iter_without_into_iter iter() method without IntoIterator for &T
clippy::into_iter_without_iter IntoIterator without iter() method
clippy::impl_trait_in_params flags verbose impl Trait in function params

10. Index and IndexMut — bracket access

implement Index to enable collection[i] syntax:

use std::ops::{Index, IndexMut};

impl<T> Index<usize> for Stack<T> {
    type Output = T;
    fn index(&self, i: usize) -> &T { &self.data[i] }
}

impl<T> IndexMut<usize> for Stack<T> {
    fn index_mut(&mut self, i: usize) -> &mut T { &mut self.data[i] }
}
let s = Stack::from(vec![10, 20, 30]);
println!("{}", s[2]);   // 30 — note: index from bottom, not top
s[0] = 99;              // requires IndexMut

[!note] index() panics on out-of-bounds, same as a Vec. if you want fallible access, add a get(i: usize) -> Option<&T> method instead and document the difference.


11. operator overloading — Add, Sub, BitOr, etc.

for set-like or math-like types, overloading operators makes the API feel natural:

use std::ops::BitOr;

// union of two stacks — just as an example
impl<T: Clone> BitOr for Stack<T> {
    type Output = Stack<T>;
    fn bitor(mut self, other: Stack<T>) -> Stack<T> {
        self.data.extend(other.data);
        self
    }
}

let combined = stack_a | stack_b;

common candidates:

trait operator typical use
Add + concatenation, union
Sub - difference
BitOr \| set union
BitAnd & set intersection
Neg unary - negate / reverse
Not ! complement / invert

only implement these when the semantics are genuinely obvious. an Add on a stack that concatenates is clear; an Add that does something surprising is worse than no Add at all.


12. Deref and DerefMut — transparent delegation

if your type is a thin wrapper around another type and you want to expose the inner type’s methods without re-implementing them all:

use std::ops::{Deref, DerefMut};

impl<T> Deref for Stack<T> {
    type Target = Vec<T>;
    fn deref(&self) -> &Vec<T> { &self.data }
}

impl<T> DerefMut for Stack<T> {
    fn deref_mut(&mut self) -> &mut Vec<T> { &mut self.data }
}

with this, any &Vec<T> method works directly on &Stack<T>:

let s = Stack::from(vec![1, 2, 3]);
println!("{}", s.capacity());   // Vec::capacity() — no re-impl needed
let slice: &[i32] = &s;         // deref coercion to slice

[!warning] Deref is for smart pointer patterns — Box<T>, Rc<T>, Arc<T>, String (derefs to str). if your type isn’t a transparent wrapper, implementing Deref to expose internals is a design smell. it lets callers bypass your abstraction by calling inner methods directly. use it deliberately.


13. Send and Sync — thread safety markers

Send and Sync are marker traits that Rust derives automatically if all fields are Send/Sync. you rarely implement them manually.

// these are auto-derived if T: Send and T: Sync
// Stack<T> is Send if T: Send
// Stack<T> is Sync if T: Sync

you only write manual impls for unsafe types (e.g. wrapping a raw pointer):

// only do this if you've manually verified the safety
unsafe impl<T: Send> Send for MyRawWrapper<T> {}
unsafe impl<T: Sync> Sync for MyRawWrapper<T> {}

14. checklist

for any collection type, work through this list:

impl what you get skip if
Default Stack::default() never skip
From<Vec<T>> Stack::from(vec), .into() input type doesn’t make sense
FromIterator<T> .collect::<Stack<_>>() never skip
Extend<T> .extend(iter) rarely skip
IntoIterator (consuming) for x in stack, .into_iter() never skip
IntoIterator for &T for x in &stack borrow iteration needed
IntoIterator for &mut T for x in &mut stack mutable borrow needed
size_hint in iterator pre-allocation hint never skip
ExactSizeIterator .len() on iterator when length is always known
DoubleEndedIterator .rev() when underlying data is reversible
Debug {:?} sensitive fields
Clone .clone() T doesn’t implement Clone
PartialEq / Eq == / != no natural equality
Hash use in HashMap / HashSet requires Eq first
PartialOrd / Ord sorting, BTreeMap only if ordering makes sense
Display {} formatting internal / debug-only types
Index / IndexMut collection[i] random access not meaningful
#[must_use] on methods compiler warning if ignored side-effect methods like pop

15. things that will burn you

[!warning] Iterator on the collection type itself. any .sum(), .count(), .collect() etc will silently drain your data. always use a separate iterator struct.

[!warning] Hash + manual PartialEq. if you override == but derive Hash, two values that compare equal may hash differently. HashMap will break silently. either derive both or implement both manually.

[!warning] Deref for convenience. implementing Deref to expose inner vec methods lets callers bypass your abstraction — they can call .clear(), .truncate(), .drain() etc directly. use it only for genuine smart-pointer wrappers.

[!warning] #[must_use] on mutating methods. pop(), push() etc are called for their side effect. flagging their return values as must-use creates warnings on legitimate code like s.pop(); (discard top).

[!note] Ord requires a total ordering. if any field is f32/f64, you cannot derive Ord or Eq. floating-point NaN != NaN, which breaks total ordering. wrap floats in an ordered newtype like ordered-float or implement manually with explicit NaN handling.

[!note] ExactSizeIterator is a contract, not a hint. if you implement it, size_hint must be exact at all times. if your iterator can produce fewer items than expected (e.g. due to filtering), don’t implement ExactSizeIterator — just implement size_hint with an upper bound.


GitHub · RSS