We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Generics and Traits step 15 of 24
Operator overloading: + is just a trait method
In Rust, most operators can be used with your own types by implementing a
trait from std::ops (or std::cmp for comparisons). For a type that is not
a built-in number, the operator expression is shorthand for a call to the
trait method:
| you write | what it means |
|---|---|
a + b |
Add::add(a, b) |
a - b |
Sub::sub(a, b) |
-a |
Neg::neg(a) |
a[i] |
*Index::index(&a, i) |
Built-in numbers such as i32 and f64 are handled directly by the
compiler, but the standard library also implements Add, Sub and the other
arithmetic traits for them. That is why generic code with a bound like
T: Add<Output = T> accepts i32.
A new type gets no operators
A newtype is a struct that wraps a single value to give it a distinct type:
struct Meters(f64);
fn total() -> Meters {
Meters(3.0) + Meters(1.0)
}
This does not compile:
error[E0369]: cannot add `Meters` to `Meters`
note: an implementation of `Add` might be missing for `Meters`
Meters does not pick up + from the f64 inside it. It also gets no -
and no < (comparison reports the same error code, E0369, and suggests
deriving PartialEq and PartialOrd). Every operator has to be implemented
or derived on purpose. That is often what you want from a newtype: because
nothing is implemented by default, Meters + Seconds stays an error unless
you write an impl that allows it.
Your task
#[derive(Clone, Copy)]
pub struct Vec2 { pub x: f64, pub y: f64 }
impl Add for Vec2 { ... } // missing: you write this
impl Sub for Vec2 { ... } // provided
impl Neg for Vec2 { ... } // provided
impl Index<usize> for Vec2 { ... } // provided
pub fn apply(ops: Vec<(String, f64, f64)>) -> Vec<(f64, f64)>
Vec2 + Vec2 must add the two vectors component by component:
(x1, y1) + (x2, y2) = (x1 + x2, y1 + y2).
apply is already written. It keeps a state vector that starts at
(0.0, 0.0) and processes the operations in order. Each operation is a tuple
(name, x, y), and each one adds exactly one pair to the output, so the output
has the same length as the input:
| name | effect on the state | pair added to the output |
|---|---|---|
add |
state = state + (x, y) |
the new state |
sub |
state = state - (x, y) |
the new state |
neg |
state = -state (x, y unused) |
the new state |
index |
unchanged (x, y unused) | (state[0], state[1]), which is the current state |
| anything else | unchanged | (0.0, 0.0) |
Names are matched exactly and are case sensitive, so "Add" or " add"
counts as "anything else". An empty list produces an empty output. For
example, the operations add (3, 4), sub (1, 1), neg, scale (2, 2)
produce (3, 4), (2, 3), (-2, -3), (0, 0), and the state after the
last step is still (-2, -3).
The arithmetic is ordinary f64 arithmetic. Inputs are finite, and the tests
keep every result finite. Negation flips the sign, so negating a 0.0
component gives -0.0, just as -0.0_f64 does.
The starter does not compile: apply uses state + arg, and without an Add
impl that is the E0369 error shown above (cannot add Vec2 to Vec2). Adding
the impl is the whole exercise; nothing else in the file needs to change.
Three details in the provided impls
type Output is an associated type. Add declares type Output; and
each impl chooses it. For Vec2 + Vec2 the result is a Vec2, but the
result type does not have to match the operands. The standard library
implements Sub for Instant with Output = Duration (the difference between
two points in time is a length of time), and Mul<u32> for Duration with
Output = Duration.
Add has a default type parameter. The declaration is
trait Add<Rhs = Self>, where Rhs is the type of the right-hand operand.
Writing impl Add for Vec2 therefore means impl Add<Vec2> for Vec2. To
support Vec2 * f64 you would write impl Mul<f64> for Vec2. Because the
right-hand type is a parameter, one type can have several impls of the same
operator trait, one per right-hand type.
Index returns a reference. The required signature is
fn index(&self, i: usize) -> &Self::Output. If you write -> f64 instead
of -> &f64, the compiler rejects the impl with E0053 ("method index has
an incompatible type for trait"). Because a[i] means *a.index(i), the
expression a[i] names a place inside a (a location in memory, like a
variable or a field, rather than a copied value): you can borrow it with &a[i]
without copying it. Assigning through it, as in a[i] = 1.0, additionally
requires the separate IndexMut trait, whose index_mut returns &mut.
Without IndexMut the assignment is error E0594. Vec2 only implements
Index.
The provided Index impl returns y for every index other than 0. That
is enough here because apply only reads indexes 0 and 1; a
general-purpose impl would usually panic on an out-of-range index, as slices
do.
Clippy and operator impls
Submissions are graded on three things: the code compiles, the tests pass,
and cargo clippy reports no warnings (warnings are treated as errors). Two
Clippy lints are related to this exercise.
clippy::suspicious_arithmetic_impl (on by default) looks for an
operator impl that uses a different operator, a common sign of a copied block
that was not fully edited:
use std::ops::Add;
#[derive(Clone, Copy)]
struct Meters(f64);
impl Add for Meters {
type Output = Meters;
fn add(self, rhs: Meters) -> Meters {
Meters(self.0 - rhs.0) // warning: suspicious use of `-` in `Add` impl
}
}
The lint only reports a method body that contains a single arithmetic
operator. A Vec2 impl has two (one per field), so copying the Sub impl
into Add and forgetting to change the - signs is not reported by
Clippy; the tests catch it instead. Its sibling lint
suspicious_op_assign_impl does the same for compound-assignment impls such
as AddAssign (+=), with the same single-operator restriction.
clippy::should_implement_trait (also on by default) reports a public
inherent method whose name and signature match a standard trait method. If
you wrote pub fn add(self, other: Vec2) -> Vec2 directly on Vec2 instead
of implementing the trait, Clippy reports "method add can be confused for
the standard trait method std::ops::Add::add" and suggests implementing
std::ops::Add or choosing a different name.
A note on types that own memory
Add::add takes self and rhs by value. For Vec2 that costs nothing
extra: it is Copy and holds two f64 values (16 bytes), so the operands are
copied and stay usable after a + b. For a type that is not Copy, such as
one that owns a heap buffer, a + b moves both operands, and the caller can
no longer use them. Two common ways around that are to also implement the
trait for references (impl Add<&Matrix> for &Matrix, so callers can write
&a + &b) or to choose a borrowed right-hand type. String takes the second
route: it implements Add<&str> for String, so s1 + &s2 moves s1, reuses
its buffer, and only borrows s2 (&s2 is a &String, which converts to
&str automatically). There is no impl for &String + &String, so that
expression is error E0369.
Your code, saved here
Sign in to access your accepted solutions and saved draft history. You can keep reading the lesson without an account.
Sign in to your accountStuck?
Rust reference solution
Sign in to track your attempts toward unlocking the reference solution.