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
- 2.
IteratorvsIntoIterator— the one people get wrong most - 3.
size_hintandExactSizeIterator - 4.
FromIteratorandExtend— the collect side - 5. conversion traits —
From,Into,TryFrom - 6. derivable traits — always think about these first
- 7.
DisplayandDebug— formatting - 8.
#[must_use]— lint for ignored return values - 9.
[lints]in Cargo.toml — crate-level lint config - 10.
IndexandIndexMut— bracket access - 11. operator overloading —
Add,Sub,BitOr, etc. - 12.
DerefandDerefMut— transparent delegation - 13.
SendandSync— thread safety markers - 14. checklist
- 15. things that will burn you
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]
HashandEqmust agree: if two values are==, their hashes must be identical. if you implementPartialEqmanually (e.g. custom equality logic), you must also implementHashmanually. never mix derivedEqwith manualHashor vice versa.
[!note]
Ordrequires a total ordering. if your type hasf32orf64fields, you can’t deriveOrdor evenEqbecause floats haveNaN. usePartialOrdonly, 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 aVec. if you want fallible access, add aget(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]
Derefis for smart pointer patterns —Box<T>,Rc<T>,Arc<T>,String(derefs tostr). if your type isn’t a transparent wrapper, implementingDerefto 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]
Iteratoron the collection type itself. any.sum(),.count(),.collect()etc will silently drain your data. always use a separate iterator struct.
[!warning]
Hash+ manualPartialEq. if you override==but deriveHash, two values that compare equal may hash differently.HashMapwill break silently. either derive both or implement both manually.
[!warning]
Dereffor convenience. implementingDerefto 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 likes.pop();(discard top).
[!note]
Ordrequires a total ordering. if any field isf32/f64, you cannot deriveOrdorEq. floating-pointNaN != NaN, which breaks total ordering. wrap floats in an ordered newtype likeordered-floator implement manually with explicitNaNhandling.
[!note]
ExactSizeIteratoris a contract, not a hint. if you implement it,size_hintmust be exact at all times. if your iterator can produce fewer items than expected (e.g. due to filtering), don’t implementExactSizeIterator— just implementsize_hintwith an upper bound.