Generics in Functions
Generics in Implementation (impl)
Traits
Bounds (Trait Bounds)
Multiple Bounds
Big Picture (one-line intuition)
Rust vs C / C++ — Concept Mapping (Big Picture)
Generics is the topic of generalizing types and functionalities to broader cases. This is extremely useful for reducing code duplication in many ways, but can call for rather involved syntax. Namely, being generic requires taking great care to specify over which types a generic type is actually considered valid. The simplest and most common use of generics is for type parameters.
A type parameter is specified as generic by the use of angle brackets and upper camel case: . "Generic type parameters" are typically represented as . In Rust, "generic" also describes anything that accepts one or more generic type parameters . Any type specified as a generic type parameter is generic, and everything else is concrete (non-generic).
Big Picture (one-line intuition)
Rust vs C / C++ — Concept Mapping (Big Picture)
Generics in Functions
When you need it
Use generic functions when:
Logic is the same
Type can vary
You don’t need stored state
Theory
fn func<T>(x: T) -> T
T is decided at compile time.
Example
fn identity<T>(x: T) -> T {
x
}
fn main() {
println!("{}", identity(10));
println!("{}", identity("Rust"));
}
Output
10
Rust
✅ Use when: only behavior varies, not data structure
14.2
Generic identity function
fn identity<T>(x: T) -> T { x }
fn main() {
println!("{}", identity(10));
println!("{}", identity(3.5));
}
Output
10
3.5
Generic swap (returns tuple)
fn swap<T, U>(a: T, b: U) -> (U, T) {
(b, a)
}
fn main() {
let s = swap("Rust", 2026);
println!("{:?}", s);
}
Output
(2026, "Rust")
Generic max (needs PartialOrd)
fn max_of<T: PartialOrd>(a: T, b: T) -> T {
if a > b { a } else { b }
}
fn main() {
println!("{}", max_of(10, 20));
println!("{}", max_of(5.5, 3.2));
}
Output
20
5.5
Generic slice first element (returns Option)
fn first<T>(slice: &[T]) -> Option<&T> {
slice.get(0)
}
fn main() {
let a = [1, 2, 3];
println!("{:?}", first(&a));
let b: [i32; 0] = [];
println!("{:?}", first(&b));
}
Output
Some(1)
None
Generic print length (accept anything that can be seen as slice)
fn print_len<T>(slice: &[T]) {
println!("len={}", slice.len());
}
fn main() {
print_len(&[10, 20, 30]);
print_len(&['a', 'b']);
}
Output
len=3
len=2
Generics in Implementation (impl)
When you need it
Use generic impl when:
You want a type to store different data
Methods should work for all those types
Theory
struct Boxed<T> { value: T }
impl<T> Boxed<T> { ... }
Example
#[derive(Debug)]
struct Boxed<T> {
value: T,
}
impl<T> Boxed<T> {
fn get(&self) -> &T {
&self.value
}
}
fn main() {
let a = Boxed { value: 100 };
let b = Boxed { value: "Rust" };
println!("{:?}", a.get());
println!("{:?}", b.get());
}
Output
100
"Rust"
✅ Use when: data itself is generic
Generic struct Boxed
.
#[derive(Debug)]
struct Boxed<T>(T);
impl<T> Boxed<T> {
fn new(x: T) -> Self { Boxed(x) }
}
fn main() {
let a = Boxed::new(10);
let b = Boxed::new("Hello");
println!("{:?}", a);
println!("{:?}", b);
}
Output
Boxed(10)
Boxed("Hello")
2) Generic struct with getter
struct Holder<T> {
value: T,
}
impl<T> Holder<T> {
fn get(&self) -> &T {
&self.value
}
}
fn main() {
let h = Holder { value: 99 };
println!("{}", h.get());
}
Output
99
3) Generic pair struct
#[derive(Debug)]
struct Pair<T, U> {
a: T,
b: U,
}
impl<T, U> Pair<T, U> {
fn new(a: T, b: U) -> Self { Pair { a, b } }
}
fn main() {
let p = Pair::new("ID", 1001);
println!("{:?}", p);
}
Output
Pair { a: "ID", b: 1001 }
4) Specialized impl for one type (impl Holder)
struct Holder<T> { value: T }
impl Holder<String> {
fn len(&self) -> usize {
self.value.len()
}
}
fn main() {
let h = Holder { value: "Ashwani".to_string() };
println!("len={}", h.len());
}
Output
len=7
5) Generic method that changes type
#[derive(Debug)]
struct Wrap<T>(T);
impl<T> Wrap<T> {
fn map<U, F>(self, f: F) -> Wrap<U>
where
F: FnOnce(T) -> U,
{
Wrap(f(self.0))
}
}
fn main() {
let w = Wrap(10).map(|x| x.to_string());
println!("{:?}", w);
}
Output
Wrap("10")
Traits
When you need it
Use traits when:
You want shared behavior
Types are unrelated but act similarly
You want polymorphism
Theory
Trait = behavior contract
Example
trait Speak {
fn speak(&self) -> &'static str;
}
struct Dog;
struct Cat;
impl Speak for Dog {
fn speak(&self) -> &'static str { "Woof" }
}
impl Speak for Cat {
fn speak(&self) -> &'static str { "Meow" }
}
fn main() {
let d = Dog;
let c = Cat;
println!("{}", d.speak());
println!("{}", c.speak());
}
Output
Woof
Meow
✅ Use when: types differ but behavior is same
Generic trait method describe()
trait Describe {
fn describe(&self) -> String;
}
struct Car { brand: String }
impl Describe for Car {
fn describe(&self) -> String {
format!("Car brand: {}", self.brand)
}
}
fn print_desc<T: Describe>(x: &T) {
println!("{}", x.describe());
}
fn main() {
let c = Car { brand: "Tesla".to_string() };
print_desc(&c);
}
Output
Car brand: Tesla
2) Trait implemented for multiple types
trait Area {
fn area(&self) -> i32;
}
struct Square(i32);
struct Rect(i32, i32);
impl Area for Square {
fn area(&self) -> i32 { self.0 * self.0 }
}
impl Area for Rect {
fn area(&self) -> i32 { self.0 * self.1 }
}
fn main() {
let s = Square(5);
let r = Rect(4, 6);
println!("{}", s.area());
println!("{}", r.area());
}
Output
25
24
3) Generic trait with associated type
trait Convert {
type Output;
fn convert(&self) -> Self::Output;
}
struct Meter(i32);
impl Convert for Meter {
type Output = i32;
fn convert(&self) -> i32 { self.0 * 100 } // to cm
}
fn main() {
let m = Meter(2);
println!("{}", m.convert());
}
Output
200
4) Generic function returning impl Trait
trait Greeting {
fn greet(&self) -> String;
}
struct User { name: String }
impl Greeting for User {
fn greet(&self) -> String {
format!("Hello {}", self.name)
}
}
fn create_user(name: &str) -> impl Greeting {
User { name: name.to_string() }
}
fn main() {
let u = create_user("Ashwani");
println!("{}", u.greet());
}
Output
Hello Ashwani
5) Generic over iterator trait
fn count_items<I>(iter: I) -> usize
where
I: IntoIterator,
{
iter.into_iter().count()
}
fn main() {
println!("{}", count_items(vec![1, 2, 3]));
println!("{}", count_items([10, 20]));
}
Output
3
2
Bounds (Trait Bounds)
When you need it
Use bounds when:
Generic code needs capabilities
Compiler must guarantee behavior
Theory
T: Display
Means: T must implement Display
Example
use std::fmt::Display;
fn show<T: Display>(x: T) {
println!("{}", x);
}
fn main() {
show(10);
show("Rust");
}
Output
10
Rust
❌ Without bounds → compiler error
✅ With bounds → safe & explicit
Bound with Display
use std::fmt::Display;
fn show<T: Display>(x: T) {
println!("Value: {}", x);
}
fn main() {
show(10);
show("Rust");
}
Output
Value: 10
Value: Rust
Bound with Clone
fn duplicate<T: Clone>(x: T) -> (T, T) {
(x.clone(), x)
}
fn main() {
let d = duplicate(String::from("Hi"));
println!("{:?}", d);
}
Output
("Hi", "Hi")
Bound with PartialOrd (comparison)
fn min_of<T: PartialOrd>(a: T, b: T) -> T {
if a < b { a } else { b }
}
fn main() {
println!("{}", min_of(10, 7));
}
Output
7
Bound with Debug
use std::fmt::Debug;
fn debug_print<T: Debug>(x: T) {
println!("{:?}", x);
}
fn main() {
debug_print((1, "hello", true));
}
Output
(1, "hello", true)
where clause bounds style
use std::fmt::Display;
fn add_and_show<T>(a: T, b: T)
where
T: std::ops::Add<Output = T> + Display + Copy,
{
let c = a + b;
println!("{}", c);
}
fn main() {
add_and_show(10, 20);
}
Output
30
Multiple Bounds
When you need it
Use multiple bounds when:
One trait is not enough
Type must support multiple behaviors
Theory
T: Display + Clone
Example
use std::fmt::Display;
fn print_twice<T: Display + Clone>(x: T) {
println!("{}", x.clone());
println!("{}", x);
}
fn main() {
print_twice(String::from("Rust"));
}
Output
Rust
Rust
✅ Use when: one capability is insufficient
Display + Clone
use std::fmt::Display;
fn print_twice<T: Display + Clone>(x: T) {
println!("{}", x.clone());
println!("{}", x);
}
fn main() {
print_twice(String::from("Rust"));
}
Output
Rust
Rust
2) Debug + PartialEq
use std::fmt::Debug;
fn is_equal<T: Debug + PartialEq>(a: T, b: T) {
println!("{:?} == {:?} => {}", a, b, a == b);
}
fn main() {
is_equal(10, 10);
is_equal("a", "b");
}
Output
10 == 10 => true
"a" == "b" => false
3) Multiple bounds with where
use std::fmt::{Debug, Display};
fn show_both<T>(x: T)
where
T: Debug + Display,
{
println!("Display: {}", x);
println!("Debug: {:?}", x);
}
fn main() {
show_both(99);
}
Output
Display: 99
Debug: 99
4) Bounds on two generic types
use std::fmt::Display;
fn pair_print<T: Display, U: Display>(a: T, b: U) {
println!("{} {}", a, b);
}
fn main() {
pair_print("ID", 5001);
}
Output
ID 5001
5) Bounds with closure (FnOnce) + Clone
fn run<T, F>(x: T, f: F) -> T
where
T: Clone,
F: FnOnce(T) -> T,
{
f(x.clone())
}
fn main() {
let out = run(10, |n| n + 5);
println!("{}", out);
}
Output
15
Top comments (0)