How to implement Add trait for custom types

Implement the Add trait for custom types by defining the add method in an impl block with the correct Output type.

When the plus sign meets custom data

You're building a 2D game engine. You have a Player struct with x and y coordinates. You want to move the player by a velocity vector. You write player.position + velocity. The compiler rejects you with E0369 (binary operation + cannot be applied to type Position). Rust doesn't know how to add your structs. It knows how to add i32 and f64, but your data is opaque to the language. You have to teach Rust the rules of addition for your type.

The Add trait lives in std::ops. It's the bridge between the + syntax and the actual logic. When you write a + b, Rust desugars that to Add::add(a, b). The trait requires an associated type Output so the compiler knows what type comes out. It's not magic. It's just a function call with nicer syntax. Implementing the trait tells the compiler that your type supports the operator and defines exactly what happens.

The Add trait contract

The Add trait has one method, add, and one associated type, Output. The method takes self and another value. The associated type declares the result. This structure keeps the type system sound. The compiler checks the types at compile time. If the result type doesn't match Output, you get a mismatch error.

The trait is generic over the right-hand side. You can write impl Add<U> for T. This lets you add different types. You can add a Point to a Point, or a Point to an i32. The generic parameter Rhs (right-hand side) defaults to Self, but you can override it. This flexibility allows mixed-type arithmetic without forcing every operand to be the same type.

Minimal implementation

Start with a simple struct. Implement Add to combine the fields. The type Output line is mandatory. It declares that adding two Point values yields a Point. The add method takes self and other by value. This means the operation consumes both inputs.

use std::ops::Add;

/// A point in 2D space with integer coordinates.
struct Point {
    x: i32,
    y: i32,
}

impl Add for Point {
    // Declare that Point + Point produces a Point.
    type Output = Point;

    // Consume both operands. Matches primitive behavior.
    fn add(self, other: Point) -> Point {
        Point {
            x: self.x + other.x,
            y: self.y + other.y,
        }
    }
}

fn main() {
    let p1 = Point { x: 1, y: 2 };
    let p2 = Point { x: 3, y: 4 };

    // Desugars to Point::add(p1, p2).
    let sum = p1 + p2;

    println!("Result: ({}, {})", sum.x, sum.y);
}

The add method returns a new Point. The original values p1 and p2 are moved into the function. If you try to use p1 after the addition, the compiler rejects you with E0382 (use of moved value). The values are gone. This matches the behavior of primitive types like i32, which are cheap to move. For small structs, consuming the inputs is efficient and idiomatic.

Convention aside: The community often implements Add with self for types that are small enough to move cheaply, typically under 16 bytes. For larger types, borrowing is preferred to avoid allocations. Check the size of your struct before deciding on the signature.

Why type Output exists

You might ask why Add uses type Output instead of a simple return type on the function. Consider mixed-type addition. You might want to add a scalar to a vector: 5 + vector. Here, Self is i32. If the trait required returning Self, the result would have to be an i32. But you want a Vector. The Output associated type decouples the result from the receiver. It allows i32 + Vector to produce a Vector.

It also supports cases where the result is a third type entirely. Imagine a Matrix type where adding two matrices produces a Result<Matrix> if overflow is possible. The Output type can be Result<Matrix>. The associated type gives the trait the flexibility to model real-world math without forcing the result to match the input. It's a design choice that prioritizes expressiveness over simplicity.

Generic types and trait bounds

Generic types add a layer of complexity. You can't just implement Add for Point<T>. The compiler needs to know that T supports addition. You add a trait bound: T: Add<Output = T>. This bound ensures that adding two T values produces a T. If T is i32, the bound holds. If T is String, the bound fails, and the implementation is disabled. This prevents you from writing Point<String> + Point<String>.

use std::ops::Add;

/// A generic point that works with any addable coordinate type.
struct Point<T> {
    x: T,
    y: T,
}

// Only implement Add if T supports addition and returns T.
impl<T: Add<Output = T>> Add for Point<T> {
    type Output = Point<T>;

    fn add(self, other: Point<T>) -> Point<T> {
        Point {
            x: self.x + other.x,
            y: self.y + other.y,
        }
    }
}

fn main() {
    let p1 = Point { x: 1.0, y: 2.0 };
    let p2 = Point { x: 3.0, y: 4.0 };

    // Works because f64 implements Add<Output = f64>.
    let sum = p1 + p2;
    println!("Float point: ({}, {})", sum.x, sum.y);
}

The Output = T part is crucial. It guarantees the result type matches the input type, which is required for the generic Point structure. Without it, the compiler can't prove that self.x + other.x produces a T. It might produce a different type, breaking the struct definition. The explicit bound makes the requirements clear.

Convention aside: When implementing Add for generic types, always include the Output = T bound. Omitting it leads to confusing error messages later when the compiler can't infer the output type. The explicit bound documents the constraint for anyone reading the code.

Borrowing for performance

Consuming inputs works for small types. For large types, moving can be expensive. You can implement Add for references to avoid copies. The community convention is to implement impl Add for &T. This covers the &a + &b case. It returns a new owned value. The inputs remain borrowed. This allows you to reuse the operands.

use std::ops::Add;

/// A large matrix that is expensive to move.
struct Matrix {
    data: Vec<f64>,
}

// Borrow both inputs. Returns a new owned Matrix.
impl Add for &Matrix {
    type Output = Matrix;

    fn add(self, other: &Matrix) -> Matrix {
        let mut result = Vec::with_capacity(self.data.len());
        for (a, b) in self.data.iter().zip(other.data.iter()) {
            result.push(a + b);
        }
        Matrix { data: result }
    }
}

fn main() {
    let m1 = Matrix { data: vec![1.0, 2.0] };
    let m2 = Matrix { data: vec![3.0, 4.0] };

    // Both matrices remain valid after the addition.
    let sum = &m1 + &m2;
    println!("Sum length: {}", sum.data.len());
}

If you only implement impl Add<&T> for T, you support a + &b, but you still can't do &a + &b. You'd need multiple implementations to cover all combinations. impl Add for &T is the pragmatic choice for read-only addition on large types. It minimizes the number of impl blocks while supporting the most common usage pattern.

Pitfall: If you implement Add for Matrix but try to add references, you get E0277 (trait bound not satisfied). The compiler looks for Add on &Matrix and finds nothing. You have to implement the reference version explicitly. Rust doesn't auto-deref for trait implementations. The trait resolution is exact.

Pitfalls and compiler errors

Errors appear when the trait contract isn't met. If you forget type Output, the compiler complains about a missing associated type. If you return the wrong type, you get E0308 (mismatched types). The compiler compares the return type of add against Output. They must match exactly.

Another common issue is mixing strategies. If you implement Add for Point but try to add references, you get E0277. You need the reference impl. If you implement Add for &Point but try to add owned values, you also get E0277. You need the owned impl. The compiler doesn't infer conversions between owned and borrowed for trait resolution. You have to provide the impl that matches the usage.

Can Add return a Result? No. The trait signature is fixed. fn add(...) -> Output. If your addition can fail, like overflow in debug mode or division by zero in a fraction type, you can't use Add. You must use a method like try_add. This is a design constraint. Add is for infallible operations. If you need fallibility, skip the trait and use a method. This keeps the operator syntax safe and predictable. Users expect + to always work. If it can fail, the method call makes the risk explicit.

The += operator uses a separate trait, AddAssign. If you implement Add but not AddAssign, users can't write p += q. They have to write p = p + q. This creates a temporary value. For performance-critical code, implement AddAssign alongside Add to support in-place mutation. The compiler won't warn you if you miss it. You have to remember to add it.

Decision matrix

Use impl Add for T when the type is small enough that moving costs nothing, or when consuming the inputs makes semantic sense. Use impl Add for &T when the type is large, or when you need to keep the original values alive after the operation. Use impl Add<U> for T when you want to support mixed-type addition, such as adding a scalar to a vector. Use AddAssign when you need in-place mutation with the += operator to avoid allocating a new value. Use a custom method like try_add when the operation can fail and you need to return a Result.

Match the implementation to the cost of the data and the semantics of the operation. The compiler enforces the contract. You define the rules.

Where to go next