Skip to content

Search is only available in production builds. Try building and previewing the site to test it out locally.

8.4 Generics

Introduction: Flexible and Safe Abstractions

Section titled “Introduction: Flexible and Safe Abstractions”

You are likely familiar with using templates and the STL in C++ for generic code, and abstract classes or interfaces for defining shared behaviour. Rust provides its own mechanisms for these goals: Generics, Traits, and Lifetimes. These features are integrated with Rust’s ownership system, allowing you to write flexible and reusable code while maintaining compile-time memory safety. This section introduces these concepts with examples and comparisons to C++.

Generics, Traits, and Lifetimes are fundamental to Rust, allowing abstract and performant code while enforcing memory safety at compile time.

  • Generics provide parametric polymorphism, allowing functions and data structures to work with various types, similar to C++ templates but with explicit trait bounds.
  • Traits define shared behaviour, acting as Rust’s equivalent of interfaces or abstract classes, enabling both static and dynamic dispatch.
  • Lifetimes are a mechanism that ensures references are valid, preventing memory errors like dangling pointers.

Understanding these concepts allows you to use Rust effectively. The initial complexity of the borrow checker and lifetime annotations results in reliable systems, which is important for performance-critical domains like edge programming.

Generics are abstract stand-ins for concrete types or other properties, enabling you to write code that works with a variety of types without duplicating logic. This is Rust’s answer to C++ templates.

Generic Functions
A generic function allows one or more parameterised types to appear in its signature. These type parameters are declared within angle brackets (<T>) after the function name. Here is an example of a generic function that finds the largest item in a list, regardless of the item’s specific type.

This has very similar syntax to templates in C++.

// This function is generic over type T
// T must implement the PartialOrd trait (comparison) and Copy (for duplication).
fn largest<T: PartialOrd + Copy>(list: &[T]) -> T {
let mut largest = list[0];
for &item in list.iter() {
if item > largest { // Requires PartialOrd
largest = item; // Requires Copy
}
}
largest // returns the largest value
}
fn main() {
let number_list = vec![10, 20, 30, 50, 40];
let result = largest(&number_list);
println!("The largest number is {}", result);
let char_list = vec!['y', 'm', 'a', 'q'];
let result = largest(&char_list);
println!("The largest char is {}", result);
}

This gives the output:

The largest number is 50
The largest char is y

In this example, T is a generic type parameter. The PartialOrd and Copy after T are “trait bounds” (discussed in the next section) that constrains T to types that can be partially ordered (compared) and copied.

In C++, templates use “duck typing”: if it quacks like a duck (i.e., supports the operations used), it’s a duck. Errors related to missing operations are caught during template instantiation. Rust’s generics, however, require explicit “trait bounds” (like PartialOrd and Copy) to declare what capabilities the generic type T must have. This allows the Rust compiler to check the generic code for correctness at the point of definition, rather than at each instantiation, leading to more clear error messages.

Generic Structs
Structs can also be defined with generic type parameters, allowing them to hold data of various types. Here is a generic struct example, which is very much like a C++ template class:

// Define a generic struct Point that can hold any type T for its coordinates.
struct Point<T> {
...
x: T,
y: T,
}
// Implement methods for the generic Point struct.
impl<T> Point<T> {
fn new(x: T, y: T) -> Self {
Point { x, y }
}
}
// We can also implement methods for specific concrete types of Point e.g., f64
impl Point<f64> {
fn distance_from_origin(&self) -> f64 {
(self.x * self.x + self.y * self.y).sqrt()
}
}
fn main() {
let integer_point = Point::new(5, 10); // Point<i32>
println!("Integer point: ({}, {})", integer_point.x, integer_point.y);
let float_point = Point::new(1.0, 2.5); // Point<f64>
println!("Float point: ({}, {})", float_point.x, float_point.y);
// Call a method specific to Point<f64>
println!("Distance from origin: {}", float_point.distance_from_origin());
// This would not compile: integer_point.distance_from_origin();
// as there is no type-specific implementation.
}

This gives the output:

Integer point: (5, 10)
Float point: (1, 2.5)
Distance from origin: 2.692582403567252

Generic Enums
Enums can also be generic, allowing their variants to hold data of various types. Option<T> and Result<T, E> from Rust’s standard library are prime examples of generic enums. Here is an example of the use of a generic enum:

use std::fmt::Debug; // Needed to apply to T for debug {:?} output
// Define a generic BoxContent enum
enum BoxContent<T> {
Empty,
Item(T),
Error(String),
}
fn process_box_content<T: Debug>(content: BoxContent<T>) {
match content {
BoxContent::Empty => println!("The box is empty."),
BoxContent::Item(item) => println!("The box contains an item: {:?}", item),
BoxContent::Error(msg) => println!("An error occurred: {}", msg),
}
}
fn main() {
let int_box = BoxContent::Item(123);
process_box_content(int_box); // Output: The box contains an item: 123
let string_box = BoxContent::Item(String::from("Rust is great!"));
process_box_content(string_box); // Output: The box contains an item: "Rust is great!"
let empty_box: BoxContent<f64> = BoxContent::Empty; // Type annotation needed for Empty
process_box_content(empty_box); // Output: The box is empty.
let error_box: BoxContent<bool> = BoxContent::Error(String::from("Failed to load."));
process_box_content(error_box); // Output: An error occurred: Failed to load.
}

This gives the output:

The box contains an item: 123
The box contains an item: "Rust is great!"
The box is empty.
An error occurred: Failed to load.
Rust concept demo

Generics & Trait Bounds in Rust

src/main.rs
1fn largest<T: PartialOrd>(list: &[T]) -> &T {
2 let mut biggest = &list[0];
3 for x in list {
4 if x > biggest { biggest = x; }
5 }
6 biggest
7}
8
9fn main() {
10 let nums = vec![34, 50, 25, 100, 65];
11 println!("largest = {}", largest(&nums));
12}
terminal — cargo run
$ cargo run
largest = 100
What just happened

`<T: PartialOrd>` declares a *type parameter* `T` together with a *trait bound* — any type used as `T` must implement `PartialOrd`, the trait that supplies `<`, `>`, `<=`, and `>=`. The bound is the contract the compiler needs in order to allow `x > biggest` inside the body. Without it, the compiler couldn't know whether `>` is even defined for whatever `T` turns out to be — see scenario 3 for what happens if you forget.

C++ comparison

C++ touchstone: this is a function template — `template <typename T> T const& largest(std::vector<T> const& v)`. Pre-C++20, the constraint was implicit and only surfaced as a wall of template-instantiation errors when `>` failed for some `T`. With C++20 *concepts* (`requires std::totally_ordered<T>`) you can finally say the constraint up front — exactly what Rust's trait bounds have done since day one.

Concept Match

Match the Generics Concepts

Traits are a fundamental concept in Rust, used to define shared behaviour that multiple types can implement. They serve as a kind of contract or interface that types must fulfil, enabling polymorphism and code reuse in a safe and expressive way. Traits in Rust are conceptually similar to interfaces in Java, typeclasses in Haskell, or abstract base classes with pure virtual functions in C++. However, Rust’s approach is strongly influenced by its emphasis on safety, zero-cost abstractions, and explicitness.

A trait defines a set of method signatures that a type can implement. Once a type implements a trait, it gains the associated behaviour and can be used in generic functions or trait objects that rely on that trait. This is a key mechanism for enabling generic programming in Rust.

Defining and Implementing a Trait
A trait defines a set of methods that a type must implement to satisfy that trait. Methods in traits only need declarations; their implementation is left to specific types. Traits can also provide default implementations for methods.

To implement a trait for a specific type, you use the impl trait for type syntax (in this case it is impl Summary for Tweet.

Here is an example of creating a Summary trait for different types of structures:

trait Summary { // This is the trait we are defining
fn summarize(&self) -> String;
}
struct Tweet { // Many structs could be used, e.g., Document, Email etc.
user: String,
content: String,
}
impl Summary for Tweet { // The "implements trait for type" statement
fn summarize(&self) -> String {
format!("@{}: {}", self.user, self.content) // using the @Twitter style
}
}
fn notify(item: &impl Summary) {
println!("Notification: {}", item.summarize());
}
fn main() {
let tweet = Tweet {
user: String::from("molloyd"),
content: String::from("Traits make Rust powerful!"),
};
notify(&tweet);
}

This code gives the output:

Notification: @molloyd: Traits make Rust powerful!

In this example we have:

  • A custom trait (Summary)
  • A struct (Tweet)
  • A trait implementation
  • A function that uses the trait via impl Trait syntax

Traits allow you to write generic code that only works with types that implement certain behaviour. This is analogous to a C++ abstract class with pure virtual functions (for required methods) and regular virtual functions (for default implementations).

A crucial rule for implementing traits is the “orphan rule”: you can implement a trait for a type only if either the trait or the type is local to your crate. This prevents you from breaking external code by adding implementations to types you don’t own.

Trait Bounds: Constraining Generics
Trait bounds allow you to specify that a generic type parameter must implement certain traits. This ensures that you can call methods defined by those traits on values of the generic type. The following example introduces constraining generics using trait bounds. It builds on the Summary trait and shows how to write functions that only accept types implementing specific traits.

use std::fmt::Display;
// Define a custom trait
trait Summary {
fn summarize(&self) -> String;
}
// A struct that implements Summary
struct Tweet {
user: String,
content: String,
}
impl Summary for Tweet {
fn summarize(&self) -> String {
format!("@{}: {}", self.user, self.content)
}
}
// A generic function using a trait bound
fn notify<T: Summary>(item: &T) {
println!("Breaking news: {}", item.summarize());
}
// A generic function using multiple trait bounds
// Requires the type to implement both Summary and Display.
fn print_and_notify<T: Summary + Display>(item: &T) {
println!("Display: {}", item);
println!("Summary: {}", item.summarize());
}
// Implement Display for Tweet so we can use it in `print_and_notify`
impl Display for Tweet {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{} says: {}", self.user, self.content)
}
}
fn main() {
let tweet = Tweet {
user: String::from("molloyd"),
content: String::from("Generics and traits are powerful!"),
};
notify(&tweet); // Uses only Summary trait
print_and_notify(&tweet); // Uses both Summary and Display traits
}

This results in the output:

Breaking news: @molloyd: Generics and traits are powerful!
Display: molloyd says: Generics and traits are powerful!
Summary: @molloyd: Generics and traits are powerful!

Trait Objects (dyn Trait): Dynamic Dispatch While trait bounds enable static dispatch (compiler generates specialised code for each concrete type), Rust also supports dynamic dispatch using trait objects. A trait object is a “fat pointer” (a pointer to the data plus a pointer to a vtable) that allows you to work with values of different concrete types that implement a specific trait, similar to C++‘s virtual functions.

Here’s an updated version of the previous example that shows both static dispatch with generics + where clause, and dynamic dispatch using a dyn trait object.

use std::fmt::Display;
// Define a trait
trait Summary {
fn summarize(&self) -> String;
}
// Struct that implements Summary and Display
struct Tweet {
user: String,
content: String,
}
impl Summary for Tweet {
fn summarize(&self) -> String {
format!("@{}: {}", self.user, self.content)
}
}
impl Display for Tweet {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{} says: {}", self.user, self.content)
}
}
// Function with generic bounds using a `where` clause
fn print_and_notify<T>(item: &T)
where
T: Summary + Display,
{
println!("Display: {}", item);
println!("Summary: {}", item.summarize());
}
// Function using a trait object for dynamic dispatch
fn print_dyn_summary(item: &dyn Summary) {
println!("(dyn) Summary: {}", item.summarize());
}
fn main() {
let tweet = Tweet {
user: String::from("rustacean"),
content: String::from("Trait bounds and dyn traits together!"),
};
// Static dispatch using generic trait bounds
print_and_notify(&tweet);
// Dynamic dispatch using a trait object
print_dyn_summary(&tweet);
}

This code gives the output:

Display: rustacean says: Trait bounds and dyn traits together!
Summary: @rustacean: Trait bounds and dyn traits together!
(dyn) Summary: @rustacean: Trait bounds and dyn traits together!
  • print_and_notify() uses static dispatch via generics and where clause. Inlined at compile time.
  • print_dyn_summary() Uses dynamic dispatch via &dyn Summary. Resolved at runtime using a vtable.

Rust’s support for both trait bounds (static dispatch) and dyn Trait (dynamic dispatch) allows you to choose between performance and flexibility without sacrificing safety. Trait bounds allow the compiler to generate efficient, specialised code with no runtime cost, which is suitable for performance-critical applications. On the other hand, dyn Trait enables runtime polymorphism, useful when working with values of different types through a common interface. This design gives Rust developers fine-grained control over abstraction and efficiency, while maintaining strong type safety and avoiding the pitfalls of inheritance-based OOP.

Common Standard Library Traits
Rust’s standard library provides several common traits that provide functionality across types. The Debug trait allows types to be formatted using {:?} for debugging output, while Display enables user-friendly formatting with {}. Clone and Copy support duplication of values, with Copy used for lightweight, implicit copying (like integers) and Clone for more complex, explicit cloning. The PartialEq and Eq traits allow for equality comparisons, and PartialOrd and Ord enable ordering and sorting. Traits like Iterator, IntoIterator, and From are also used for collections and conversions. These traits are part of idiomatic Rust, enabling generic programming and interoperability across types. Some of the most common standard library traits are described in Table 1.

Table 1: Shows a collection of Standard Library Traits and their C++ Analogy

TraitPurposeExample UseC++ Analogy
CloneExplicit deep copyvalue.clone()Copy Constructor
CopyImplicit bitwise copy (for simple types)let y = x; (if x is Copy)Trivial Copy Constructor
DebugEnable {:?} debug printing#No direct analogy; requires manual operator<< or debugger support
DisplayEnable {} user-facing printingimpl fmt::Display for MyTypeoperator<< for std::ostream
PartialEq, EqEquality comparison (==)a == boperator==
PartialOrd, OrdOrdering comparison (<, >, etc.)a < boperator<
IteratorDefine iteration behaviourfor item in collection.iter()std::iterator concepts, range-based for loops
From<T>, Into<U>Value-to-value conversionsString::from("hello")Implicit/explicit constructors, static_cast
DefaultProvide a default valueMyStruct::default()Default constructor
ErrorBase trait for custom error typesimpl Error for MyCustomErrorstd::exception
SendSafe to transfer across threads(Auto-implemented or manually for raw pointers)No direct analogy; relies on programmer discipline
SyncSafe to share references across threads(Auto-implemented or manually for raw pointers)No direct analogy; relies on programmer discipline
Rust concept demo

Traits & `impl` in Rust

src/main.rs
1trait Greet {
2 fn hello(&self);
3}
4
5struct Dog;
6
7impl Greet for Dog {
8 fn hello(&self) {
9 println!("woof!");
10 }
11}
12
13fn main() {
14 let d = Dog;
15 d.hello();
16}
terminal — cargo run
$ cargo run
woof!
What just happened

A `trait` declares a set of method signatures any type can opt in to. An `impl Trait for Type` block supplies the bodies. Call the method on a value with the usual `d.hello()` dot-syntax — the compiler resolves it to `Dog`'s implementation at compile time. There's no inheritance involved: `Dog` is a plain struct that just happens to satisfy the `Greet` contract.

C++ comparison

C++ touchstone: a Rust trait is conceptually a pure-virtual interface or a C++20 concept. The `impl Greet for Dog` block is the equivalent of `class Dog : public Greet` plus method definitions — but Rust splits the type definition and the interface conformance into separate blocks, so `Dog` doesn't need to know about `Greet` at the point it's defined.

Concept Match

Match the Trait Concepts

Code Cloze
Rust

Identify Lifetime Annotations

Lifetimes are a part of Rust’s ownership system that ensure references remain valid, preventing “dangling references” at compile time. They are a form of generics that apply to references.

The Problem of Dangling References
A dangling reference occurs when a reference points to memory that has already been deallocated or gone out of scope. In C/C++, this leads to undefined behaviour.

C++ Example (Dangling Pointer):

int* create_dangling_ptr() {
int x = 5; // x is on the stack
return &x; // Returns address of local variable
} // x is deallocated here
int main() {
int* ptr = create_dangling_ptr();
std::cout << *ptr; // Undefined behaviour: dereferencing a dangling pointer
return 0;
}

Rust’s compiler prevents this by ensuring that the “lifetime of a reference” (how long it’s used) does not exceed the “lifetime of the value” it points to.

-// This Rust code would cause a compile-time error:
-fn dangle() -> &String { // ERROR: missing lifetime specifier
- let s = String::from("hello"); // s is created here
- &s // Attempting to return a reference to s
-} // ERROR: s does not live long enough. s is dropped here.
+fn no_dangle() -> String { // Corrected: Return ownership
+ let s = String::from("hello");
+ s // Ownership of `s` is moved out of the function
+}
fn main() {
let s = no_dangle(); // `s` in main now owns the String
println!("{}", s); // Valid
}

This compile-time check incurs no runtime overhead for temporal safety.

Lifetime Annotations ('a)
While lifetimes are always present, the compiler can often infer them. However, in more complex scenarios, especially when functions take or return references, explicit lifetime annotations are required to help the borrow checker understand the relationships between references. The syntax for lifetime annotation is an apostrophe followed by a lowercase letter, like 'a.

// This function takes two string slices and returns a string slice.
// The returned slice's lifetime is tied to the *shorter* of the two input lifetimes.
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
fn main() {
let string1 = String::from("abcd");
let string2 = "xyz"; // String literal has 'static lifetime
let result = longest(string1.as_str(), string2);
println!("The longest string is {}", result); // Output: The longest string is abcd
// Example demonstrating lifetime constraints:
let string3 = String::from("long string is long");
{
let string4 = String::from("xyz");
let result_inner = longest(string3.as_str(), string4.as_str());
println!("The longest string in inner scope is {}", result_inner);
} // string4 goes out of scope here. result_inner's lifetime ends here.
// println!("{}", result_inner); // This would be a compile-time error
}

This code gives the output:

The longest string is abcd
The longest string in inner scope is long string is long

The 'a annotation tells the compiler that the returned reference will be valid for the duration of the shortest of the two input references. C++ lacks direct lifetime annotations and compile-time checks for reference validity. Developers must manually ensure that references do not outlive the data they point to.

Lifetime Elision Rules
Rust’s compiler can often infer lifetimes automatically, a process called “lifetime elision.” This reduces the need for explicit annotations in common patterns.

Common Elision Rules:

  • If there’s exactly one input lifetime (e.g., fn foo(x: &str)), that lifetime is assigned to all elided output lifetimes (e.g., -> &str).
  • If a method has &self or &mut self as its first parameter, the lifetime of self is assigned to all elided output lifetimes.

For example:

// This function's lifetimes are elided, but Rust infers them as:
// fn first_word<'a>(s: &'a str) -> &'a str
fn first_word(s: &str) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}
&s[..]
}
fn main() {
let my_string = String::from("hello world");
let word = first_word(&my_string);
println!("First word: {}", word);
}

Which gives the output:

First word: hello

The 'static Lifetime
The 'static lifetime is a special lifetime that indicates a reference is valid for the entire duration of the program.

// String literals have 'static lifetime
// Use SCREAMING_SNAKE_CASE by convention
static S: &str = "I live for the entire program!";
// Static variables also have 'static lifetime
static APP_VERSION: &str = "1.0.0";
fn print_static_ref(s: &'static str) { // cannot use the same name as static s
println!("Static reference: {}", s);
}
fn main() {
print_static_ref(S);
println!("App version: {}", APP_VERSION);
// A type parameter with a 'static bound does not contain any non-static references.
// Owned data always passes a 'static lifetime bound.
fn process_static<T: std::fmt::Debug + 'static>(data: T) {
println!("Processing static data: {:?}", data);
}
let my_owned_string = String::from("This string is owned and 'static");
process_static(my_owned_string); // OK: String is owned data
// process_static(&my_owned_string); // ERROR: &String only lives for main's scope
}

This code gives the output:

Static reference: I live for the entire program!
App version: 1.0.0
Processing static data: "This string is owned and 'static"

Key Characteristics of static: \

  • 'static Lifetime: Values declared with static inherently have the 'static lifetime. This means they are stored directly in the program’s binary and live for the entire duration of the program’s execution.
  • Immutability (by default): Static values are immutable by default. You cannot change their value after they are initialised.
  • Initialisation: They must be initialised with a constant value at compile time. This means the value must be known and fixed when the program is compiled. You cannot initialize a static with a value that requires runtime computation (e.g., let x = format!("hello"); static MY_STRING: &str = &x; would not work).
  • Naming Convention: By convention, static variables in Rust are named in SCREAMING_SNAKE_CASE.

Key Characteristics of const vs. static: \

  • const values are inlined wherever they are used. This means they don’t necessarily have a single fixed memory address like static values do. The compiler might just copy the value directly into the code where it’s needed.
  • If you use a const multiple times, the compiler might create multiple copies of the value.
  • Think of const as a fancy macro that gets replaced by its literal value at compile time.
  • Used for true constants, often numerical, or small, frequently used strings where you don’t need a guaranteed single memory location.
  • By convention, const variables are also named in SCREAMING_SNAKE_CASE.
Rust concept demo

Lifetimes in Rust

src/main.rs
1fn main() {
2 let r;
3 {
4 let x = 5;
5 r = &x;
6 }
7 println!("r = {r}");
8}
terminal — cargo build
$ cargo build
error[E0597]: `x` does not live long enough
--> src/main.rs:5:13
|
4 | let x = 5;
| - binding `x` declared here
5 | r = &x;
| ^^ borrowed value does not live long enough
6 | }
| - `x` dropped here while still borrowed
7 | println!("r = {r}");
| - borrow later used here
What the compiler is telling you

Every value has a **lifetime** — the span of code during which it is alive. `x` is declared inside the inner block, so its lifetime ends at the closing `}`. But `r` is declared in the outer scope and is still in use on the `println!` line. A reference cannot outlive what it points at, so the compiler refuses. This is the classic dangling-pointer bug — caught at compile time, before the program ever runs.

Closures: Anonymous Functions with Captured Environment

Section titled “Closures: Anonymous Functions with Captured Environment”

Closures are anonymous functions that can capture variables from their enclosing scope. Their syntax is |parameters| body, where types are usually inferred. They are directly analogous to lambda functions in C++ (introduced in C++11), but their interaction with captured variables is governed by Rust’s ownership and borrowing system, preventing an entire class of dangling-reference bugs.

Code Cloze
Rust

Fixing a Dangling Reference

Basic Closure Syntax
The simplest closures take parameters and return a value without capturing any environment:

fn main() {
let add = |a: i32, b: i32| a + b; // Types annotated explicitly
let double = |x| x * 2; // Type inferred from context
println!("3 + 5 = {}", add(3, 5)); // Output: 3 + 5 = 8
println!("7 doubled = {}", double(7)); // Output: 7 doubled = 14
}

This gives the output:

3 + 5 = 8
7 doubled = 14

Capturing the Environment
Closures may borrow or move variables from the enclosing scope. Rust automatically infers the least restrictive capture mode the body requires:

fn main() {
let threshold = 10; // captured by immutable borrow
let above = |x| x > threshold;
println!("15 above threshold? {}", above(15)); // Output: 15 above threshold? true
println!("5 above threshold? {}", above(5)); // Output: 5 above threshold? false
}

This gives the output:

15 above threshold? true
5 above threshold? false

The Three Closure Traits: Fn, FnMut, and FnOnce
Every closure automatically implements one or more of three traits, determined by how its body uses captured variables. They form a subset hierarchy: every Fn is also a FnMut, and every FnMut is also a FnOnce.

  • Fn: borrows captures immutably; callable any number of times without side effects on captured state.
  • FnMut: borrows captures mutably; callable any number of times, but requires exclusive access during each call.
  • FnOnce: takes ownership of captured variables; callable exactly once because the first call moves the captured data out of the closure.
fn main() {
// Fn: reads captured value, callable repeatedly
let limit = 100;
let check = |x| x < limit; // Implements Fn
println!("50 < 100? {}", check(50)); // Output: 50 < 100? true
println!("200 < 100? {}", check(200)); // Output: 200 < 100? false
// FnMut: mutates captured value, callable many times
let mut count = 0;
let mut increment = || { count += 1; count }; // Implements FnMut
println!("Count: {}", increment()); // Output: Count: 1
println!("Count: {}", increment()); // Output: Count: 2
drop(increment); // Release the mutable borrow on count
println!("Final count: {}", count); // Output: Final count: 2
// FnOnce: moves captured value out on first call; callable exactly once
let data = vec![1, 2, 3];
let consume_vec = || data; // Implements FnOnce (body moves data out)
let recovered = consume_vec();
println!("Recovered: {:?}", recovered); // Output: Recovered: [1, 2, 3]
// consume_vec(); // ERROR: cannot call closure more than once
}

This gives the output:

50 < 100? true
200 < 100? false
Count: 1
Count: 2
Final count: 2
Recovered: [1, 2, 3]

The move Keyword
By default, closures capture by reference where possible. The move keyword forces the closure to take ownership of all captured variables. This is required when the closure must outlive its enclosing scope; for example, when returning a closure from a function or passing one to a thread:

fn make_adder(offset: i32) -> impl Fn(i32) -> i32 {
move |x| x + offset // offset is moved into the closure so it outlives make_adder
}
fn main() {
let add_five = make_adder(5);
let add_ten = make_adder(10);
println!("{}", add_five(3)); // Output: 8
println!("{}", add_ten(3)); // Output: 13
}

This gives the output:

8
13

Without move, the closure would hold a reference to offset on make_adder’s stack frame. Once make_adder returns, that frame is gone: a dangling reference. With move, offset is copied into the closure’s own storage, making the returned closure completely self-contained.

Closures vs C++ Lambda Functions
Rust closures are directly analogous to C++ lambda expressions:

  • C++ [&](int x){ return x + n; } captures n by reference: similar to Rust’s default borrow capture |x| x + n.
  • C++ [=](int x){ return x + n; } captures n by value: similar to Rust’s move |x| x + n.

The key difference is that Rust’s ownership system statically verifies that a captured reference does not outlive its referent, eliminating the undefined behaviour that results in C++ when a lambda outlives its captured variables.

Closures for Edge Programming
Closures are useful for configuring callbacks, event handlers, and processing pipelines. Because they are monomorphised at compile time when used through generic Fn bounds, they carry no runtime overhead compared to regular functions: an important property for resource-constrained embedded targets.

Concept Match

Match the Closure Traits and Keywords

Iterators: Lazy, Composable Data Processing

Section titled “Iterators: Lazy, Composable Data Processing”

An iterator in Rust is any value that implements the Iterator trait, which requires only one method, next(), returning Option<Self::Item>: Some(value) for the next element, or None when exhausted. All of Rust’s iterator adaptors and consumers are built on this single method.

Iterators are lazy: no work is performed until the iterator is consumed. This allows chains of operations to be composed without intermediate heap allocations.

Every standard collection provides three iterator constructors:

  • .iter(): yields immutable references (&T), leaving the collection intact.
  • .iter_mut(): yields mutable references (&mut T), allowing in-place modification.
  • .into_iter(): consumes the collection and yields owned values (T).

Iterator Adaptors
Adaptors transform one iterator into another and are lazy: they produce no values until a consumer drives the chain. Common adaptors include:

  • map(|x| ...) applies a transformation to each element.
  • filter(|x| ...) retains only elements for which the predicate returns true.
  • enumerate() pairs each element with its zero-based index as (usize, T).
  • zip(other) combines two iterators into an iterator of pairs.
  • take(n) / skip(n) limit or offset the sequence.
  • flat_map(|x| ...) maps then flattens one level of nesting.

Iterator Consumers
Consumers drive the iterator to completion and produce a final result:

  • collect::<Vec<_>>() gathers elements into a collection (requires a type hint or turbofish).
  • sum() / product() reduces to a numeric total or product.
  • fold(init, |acc, x| ...) general reduction to a single value.
  • for_each(|x| ...) executes a closure for each element (side effects only).
  • count() counts elements consumed.
  • any(|x| ...) / all(|x| ...) short-circuit predicate tests.

Rust Example: Iterator Pipeline

fn main() {
let readings = vec![12, 45, 7, 93, 28, 61, 4];
// Build a lazy pipeline: copy values out, keep those above 10, then double them
let processed: Vec<i32> = readings.iter()
.copied() // &i32 → i32 (copies each element)
.filter(|x| *x > 10) // retain readings above 10
.map(|x| x * 2) // double each surviving value
.collect(); // drive the pipeline and gather into a Vec
println!("Processed: {:?}", processed);
// fold to compute the sum (equivalent to .sum() for integers)
let total: i32 = readings.iter().fold(0, |acc, &x| acc + x);
println!("Total: {}", total);
// enumerate: print index alongside each value
for (i, val) in readings.iter().enumerate() {
print!("[{}]={} ", i, val);
}
println!();
}

This gives the output:

Processed: [24, 90, 186, 56, 122]
Total: 250
[0]=12 [1]=45 [2]=7 [3]=93 [4]=28 [5]=61 [6]=4

Implementing a Custom Iterator
Any struct can become an iterator by implementing the Iterator trait. This is particularly useful on edge devices for modelling hardware register streams, circular buffers, or sensor polling sequences:

struct Counter {
current: u32,
max: u32,
}
impl Counter {
fn new(max: u32) -> Self {
Counter { current: 0, max }
}
}
impl Iterator for Counter {
type Item = u32;
fn next(&mut self) -> Option<Self::Item> {
if self.current < self.max {
let value = self.current;
self.current += 1;
Some(value)
} else {
None
}
}
}
fn main() {
let sum: u32 = Counter::new(5).sum();
println!("Sum of 0..5: {}", sum); // Output: Sum of 0..5: 10
let evens: Vec<u32> = Counter::new(10)
.filter(|x| *x % 2 == 0)
.collect();
println!("Evens: {:?}", evens); // Output: Evens: [0, 2, 4, 6, 8]
}

This gives the output:

Sum of 0..5: 10
Evens: [0, 2, 4, 6, 8]

Once next() is implemented, every adaptor and consumer in the standard library becomes available for free. This is the power of building on a single, composable abstraction.

C++ Comparison
Rust’s iterator adaptors (map, filter, fold) are directly analogous to C++20’s ranges library (std::views::transform, std::views::filter, std::ranges::fold_left). Both systems are lazy and composable. Prior to C++20, similar pipelines required verbose combinations of <algorithm> functions (std::transform, std::copy_if) and back inserters.

Iterators for Edge Programming
Because iterator pipelines are monomorphised at compile time, they carry no runtime overhead compared to hand-written loops: the compiler routinely fuses the entire chain into a single tight loop with no intermediate allocations. On memory-constrained devices, the laziness of iterators is particularly valuable: a pipeline processes one element at a time, regardless of the size of the input.

Summary: Generics, Traits, Lifetimes, Closures, and Iterators

Section titled “Summary: Generics, Traits, Lifetimes, Closures, and Iterators”

Generics, traits, lifetimes, closures, and iterators provide Rust’s combination of safety and abstraction.

  • Generics reduce code duplication. Unlike C++ templates, Rust’s generics use explicit trait bounds, allowing the compiler to verify code at the point of definition.
  • Traits define shared behaviour. They are similar to C++ abstract base classes and interfaces, supporting both static dispatch (via monomorphisation) and dynamic dispatch (via dyn Trait).
  • Lifetimes are used by the compiler to verify that references are valid. They eliminate dangling pointers at compile time without runtime cost.
  • Closures are anonymous functions that capture their environment. The closure traits (Fn, FnMut, FnOnce) model how a closure interacts with its captured state, and the move keyword provides control over ownership.
  • Iterators are composable sequences for data processing. They are monomorphised at compile time, meaning they often have the same performance as hand-written loops, and their laziness avoids intermediate allocations.

For C++ engineers, these features mean that categories of runtime bugs—such as dangling pointers and mismatched types—are caught by the compiler. For edge programming, where correctness is required and debugging is difficult, this compile-time model is an advantage over traditional C++ approaches.

Concept Match

Match the Abstraction Concepts

Drag each definition into its matching concept slot, then click Submit. Tap × to return a placed card to the pool.

Generics
drag a definition here…
Traits
drag a definition here…
Lifetimes
drag a definition here…
Closures
drag a definition here…
Iterators
drag a definition here…

Definition Pool

A system that ensures references remain valid for as long as they are needed, preventing dangling pointers.
A mechanism that allows functions and data structures to work with multiple types using type parameters.
Lazy, composable sequences that produce values one at a time via a next() method.
Defines shared behaviour as a set of method signatures, acting as a contract for types to implement.
Anonymous functions that can capture variables from their enclosing scope, similar to C++ lambdas.
Code Cloze
Rust

Complete the Generic Struct with Trait Bounds

Code Cloze
Rust

Build a Rust Iterator Pipeline

Quiz
Select 0/1

Why does Rust require explicit 'trait bounds' (e.g., <T: PartialOrd>) for generic types?

Quiz
Select 0/1

When is the 'turbofish' syntax (::<>) typically required in Rust code?

Quiz
Select 0/1

What is the primary trade-off when using trait objects (dyn Trait) for dynamic dispatch?

Quiz
Select 0/1

Why is the 'move' keyword often used when passing a closure to a new thread or returning it from a function?

Quiz
Select 0/1

What does it mean for Rust iterators to be 'lazy'?