Skip to content

← Generics and Traits step 15 of 24

Medium Primitives

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.