Rust
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Ownership and Borrowing
-
Section 4: Structuring Data
-
Section 5: Collections and Error Handling
-
Section 6: Traits and Generics
-
Section 7: Concurrency
-
Section 8: Building for the Web
-
Section 9: Memory and Performance
-
Section 10: More Standard Library and Ecosystem
-
Section 11: Advanced Rust
-
Section 12: Rust for Systems and WebAssembly
-
Section 13: Tooling and Best Practices
-
Section 14: Data Structures and Algorithms in Rust
-
Section 15: Practical Projects
-
Section 16: Interview Practice
-
Section 17: std::collections In Depth
-
Section 18: std::io and std::fs In Depth
-
Section 19: std::net
-
Section 20: std::option and std::result In Depth
-
Section 21: std::iter In Depth
-
Section 22: std::sync In Depth
-
Section 23: std::string and std::str
-
Section 24: Cargo and Crates.io Ecosystem
-
Section 25: Popular Crates Ecosystem
-
Section 26: Rust Memory Model Deep Dive
-
Section 27: More Practice Exercises
-
Section 28: More Interview Practice
-
Section 29: Async Rust Deep Dive
-
Section 30: Tokio Ecosystem In Depth
-
Section 31: Error Handling Ecosystem Deep Dive
-
Section 32: Serde In Depth
-
Section 33: Web Development with Rust Deep Dive
-
Section 34: Database Access Ecosystem
-
Section 35: Rust for Embedded Systems Deep Dive
-
Section 36: Rust Macros In Depth
-
Section 37: Advanced Trait System
-
Section 38: Unsafe Rust In Depth
-
Section 39: Rust CLI Development
-
Section 40: Testing Ecosystem Deep Dive
-
Section 41: WebAssembly Deep Dive
-
Section 42: Rust Design Patterns
-
Section 43: More Data Structures in Rust
-
Section 44: Final Practice Projects
-
Section 45: Rust Performance Optimization
-
Section 46: Rust Ecosystem Tooling
-
Section 47: More Interview and Review
38: Operator Overloading via Traits
I've seen this happen to almost every developer moving from C++ or Python into Rust. You're building something—maybe a game engine or a physics simulation—and you create a simple struct to represent a coordinate. It feels natural to just add two coordinates together. Then, you hit this.
struct Vector2 {
x: f64,
y: f64,
}
fn main() {
let v1 = Vector2 { x: 1.0, y: 2.0 };
let v2 = Vector2 { x: 3.0, 4.0 };
let result = v1 + v2; // ❌ Error here
println!("Result: {}, {}", result.x, result.y);
}
The "cannot add" compiler wall
If you try to compile that, Rust will stop you dead. The error message is pretty blunt: cannot add `Vector2` to `Vector2`. It'll tell you that the trait std::ops::Add is not implemented for your type.
Coming from other languages, you might expect the compiler to just "figure it out" or allow you to define a method called add. But in Rust, the + symbol is actually syntactic sugar for a trait method. When you write a + b, Rust is effectively calling a.add(b). If the trait isn't implemented, the symbol simply doesn't exist for your type.
Teaching Rust how to add your structs
To fix this, we need to implement the Add trait from the std::ops module. The tricky part for beginners is the Output associated type. You have to explicitly tell Rust what the resulting type of the addition will be—even if it's just the same type you're adding.
use std::ops::Add;
#[derive(Debug, PartialEq)]
struct Vector2 {
x: f64,
y: f64,
}
impl Add for Vector2 {
type Output = Vector2; // We tell Rust: "Adding two Vector2s results in a Vector2"
fn add(self, other: Vector2) -> Vector2 {
Vector2 {
x: self.x + other.x,
y: self.y + other.y,
}
}
}
fn main() {
let v1 = Vector2 { x: 1.0, y: 2.0 };
let v2 = Vector2 { x: 3.0, y: 4.0 };
let result = v1 + v2; // Now this works!
println!("Result: {:?}", result);
}
I should mention something important here: notice that the add method takes self and other by value. This means v1 and v2 are consumed by the addition. If you try to use v1 again after the addition, you'll get a "use of moved value" error. This is where most people get frustrated with operator overloading in Rust.
Dealing with the ownership dance
In a real-world project, you rarely want to destroy your vectors just to add them. You probably want to add references to vectors. To do that, you have to implement the trait specifically for the reference types. It looks a bit repetitive, but it's the only way to keep your data alive.
Here is how I usually handle this when I want to avoid unnecessary allocations or moves:
impl Add for &Vector2 {
type Output = Vector2;
fn add(self, other: &Vector2) -> Vector2 {
Vector2 {
x: self.x + other.x,
y: self.y + other.y,
}
}
}
// Now you can do this:
let result = &v1 + &v2;
// v1 and v2 are still available for use here!
I'll leave you with one tip: if you find yourself implementing Add, you almost certainly want to implement AddAssign (the += operator) as well. It's a separate trait, but it's usually more efficient because it modifies the existing struct in-place instead of creating a brand new one.
📋 Practical Task
Implementing a Complex Number Addition Engine
Create a struct named Complex that represents a complex number with two fields: re (real part) and im (imaginary part), both as f64.
Your task is to implement the following:
- Implement
std::ops::AddforComplexso that two complex numbers can be added using the+operator. - Implement
std::ops::SubforComplexso that they can be subtracted using the-operator. - Implement
std::ops::AddAssignforComplexto support the+=operator.
Verification: In your main function, create two Complex numbers, add them together, subtract one from the other, and use += to modify one of them. Print the results to the console to ensure the math is correct (e.g., (1.0 + 2.0i) + (3.0 + 4.0i) = 4.0 + 6.0i).
There are no comments for now.