Operator traits: Add, Index, Display

Rust has no operator overloading statement. An operator is a trait method with a fixed name, and implementing the trait gives the operator to a type: + is Add, [] is Index, {} is Display and == is PartialEq. The compiler only accepts an operator where the trait exists.

Coming from C++? Operator overloading adds money in both languages and then adds an integer to it: C++ builds a temporary through a constructor, Rust needs an implementation for that exact type.

The lesson covers the four traits a value type usually needs, one section each, and ends with the error for an operand the implementation does not accept.

Add

impl Add for Point defines what + does, with the associated type Output naming the result. The method takes self by value here, so the left operand is moved into the call.

The program adds two points field by field and prints the result with the debug form. The output is the new point, and the two operands are gone.

use std::ops::Add; #[derive(Debug)] struct Point { x: i32, y: i32, } impl Add for Point { type Output = Point; fn add(self, other: Point) -> Point { Point { x: self.x + other.x, y: self.y + other.y, } } } fn main() { let sum = Point { x: 1, y: 2 } + Point { x: 3, y: 4 }; println!("{:?}", sum); }

rustc --edition 2024 add.rs
./add

Point { x: 4, y: 6 }

Taking &self and a reference operand would let the caller keep the values; the standard library does that for some types. The choice is part of the API, and the lesson on methods explains the receivers.

Index

Index<usize> gives square brackets to a type. The method returns a reference to the element, so the caller may read it but not move it out, and an out-of-range index is the implementation's job to handle.

The program wraps two numbers in a struct and reads both through brackets. The output is the two values, and the index is checked by the array inside.

use std::ops::Index; struct Pair { values: [i32; 2], } impl Index<usize> for Pair { type Output = i32; fn index(&self, index: usize) -> &i32 { &self.values[index] } } fn main() { let pair = Pair { values: [7, 9] }; println!("{} {}", pair[0], pair[1]); }

rustc --edition 2024 index.rs
./index

7 9

A mutable index needs IndexMut as well, which the vector implements. The lesson on vectors shows the bounds check that comes with it.

Display

Display is the trait behind the empty braces. The method receives a formatter and writes into it, and write! returns a Result that the method passes on.

The program prints a point as a pair of parentheses and a comma. The same implementation works in format!, in to_string and in every macro that takes a format string.

use std::fmt; struct Point { x: i32, y: i32, } impl fmt::Display for Point { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "({}, {})", self.x, self.y) } } fn main() { let point = Point { x: 1, y: 2 }; println!("{point}"); }

rustc --edition 2024 display.rs
./display

(1, 2)

The debug form is a separate trait, and the lesson on formatting shows the placeholder that selects it. A type may implement both, and the debug one can be derived.

PartialEq

PartialEq defines == and != through one method, eq. A hand-written implementation can compare only the fields that matter, which is the point of writing it instead of deriving it.

The program compares versions by their major number and prints the three answers. The minor field is still printed separately, so the value is not hidden.

#[derive(Debug)] struct Version(u32, u32); impl PartialEq for Version { fn eq(&self, other: &Self) -> bool { self.0 == other.0 } } fn main() { let a = Version(1, 0); let b = Version(1, 9); let c = Version(2, 0); println!("{} {} {} {}", a == b, a == c, a != b, b.1); }

rustc --edition 2024 partial_eq.rs
./partial_eq

true false false 9

An implementation must be consistent: equal values must stay equal, and equality should agree with a matching Hash when the type is used as a key. The lesson on HashMap relies on both.

An operand of the wrong type

The right operand of + is the trait parameter, and an implementation for Money does not accept an integer. Writing point + point when only Point + i32 exists is E0308, and the message names both types.

The program implements Add<i32> and then adds a point to a point. The compiler reports the expected integer and the found struct, which is the whole rule in one line.

use std::ops::Add; struct Point { x: i32, y: i32, } impl Add<i32> for Point { type Output = Point; fn add(self, other: i32) -> Point { Point { x: self.x + other, y: self.y } } } fn main() { let point = Point { x: 1, y: 2 }; let moved = Point { x: 3, y: 4 } + point; println!("{} {}", moved.x, moved.y); }

rustc --edition 2024 wrong_rhs_err.rs

error[E0308]: mismatched types
--> wrong_rhs_err.rs:18:40
|
18 | let moved = Point { x: 3, y: 4 } + point;
| ^^^^^ expected `i32`, found `Point`

error: aborting due to 1 previous error

For more information about this error, try `rustc --explain E0308`.

Adding an integer to money is a second implementation, impl Add<i32> for Money, and then the call compiles. The explicit list of accepted operands is the difference from an implicit conversion.

Exercises

  1. Implement Add for a money type with cents, and print the sum.
  2. Add Display to the same type and print it with braces instead of the debug form.
  3. Implement Index<usize> for a pair and read both elements with brackets.

Questions

A new set of questions in a new order every time. Choose one answer for each, then check.

  1. What is an operator in Rust?

    Answer

    A trait method with a fixed name

    The operator syntax maps to a trait method, and implementing the trait enables it.

  2. Which trait provides +?

    Answer

    Add

    std::ops::Add defines the method add and the associated Output type.

  3. Which trait provides the empty braces in a format string?

    Answer

    Display

    The empty placeholder calls Display::fmt; the debug placeholder calls Debug.

  4. What does the right-hand type of Add control?

    Answer

    Which other operand the implementation accepts

    Add<Rhs> names the other operand, and only that type is accepted for the operator.

  5. What does this program print? use std::ops::Add; #[derive(Debug)] struct Point { x: i32, y: i32, } impl Add for Point { type Output = Point; fn add(self, other: Point) -> Point { Point { x: self.x + other.x, y: self.y + other.y } } } fn main() { let sum = Point { x: 1, y: 2 } + Point { x: 3, y: 4 }; println!("{:?}", sum); }

    Answer

    Point { x: 4, y: 6 }

    The implementation adds the fields, and the debug form prints the new point.

  6. What does this program print? use std::ops::Index; struct Pair { values: [i32; 2], } impl Index<usize> for Pair { type Output = i32; fn index(&self, index: usize) -> &i32 { &self.values[index] } } fn main() { let pair = Pair { values: [7, 9] }; println!("{} {}", pair[0], pair[1]); }

    Answer

    7 9

    The brackets call index, which returns each element of the array.

  7. What does this program print? use std::fmt; struct Point { x: i32, y: i32, } impl fmt::Display for Point { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "({}, {})", self.x, self.y) } } fn main() { let point = Point { x: 1, y: 2 }; println!("{point}"); }

    Answer

    (1, 2)

    The implementation writes the fields in parentheses, and the braces use it.

  8. What does this program print? #[derive(Debug)] struct Version(u32, u32); impl PartialEq for Version { fn eq(&self, other: &Self) -> bool { self.0 == other.0 } } fn main() { let a = Version(1, 0); let b = Version(1, 9); let c = Version(2, 0); println!("{} {} {}", a == b, a == c, a != b); }

    Answer

    true false false

    The comparison uses only the first field, so a equals b, a differs from c, and a != b is false.

  9. What is the first line of the compiler output for this program? use std::ops::Add; struct Point { x: i32, y: i32, } impl Add<i32> for Point { type Output = Point; fn add(self, other: i32) -> Point { Point { x: self.x + other, y: self.y } } } fn main() { let point = Point { x: 1, y: 2 }; let moved = Point { x: 3, y: 4 } + point; println!("{} {}", moved.x, moved.y); }

    Answer

    error[E0308]: mismatched types

    The implementation accepts an integer and the call passes a point, and the message names both types.

  10. What does this program print? use std::ops::Add; #[derive(Debug)] struct Money { cents: i32, } impl Add for Money { type Output = Money; fn add(self, other: Money) -> Money { Money { cents: self.cents + other.cents } } } fn main() { let total = Money { cents: 250 } + Money { cents: 50 }; println!("{:?}", total); }

    Answer

    Money { cents: 300 }

    The two amounts add to three hundred cents, and the debug form prints the struct.

The code on this page was built with rustc 1.96.1 and run on Alpine Linux 3.24.3 (x86_64) in a Docker container.

Article author: Arthur Isaev