Brief

You can make your own types support arithmetic and other operators, just by implementing a few built-in traits. This is called operator overloading, and the effect is much like operator overloading in C++, C#, Python, and Ruby.

Arithmetic and Bitwise Operators

In Rust, the expression a + b is actually shorthand for a.add(b), a call to the add method of the standard library’s std::ops::Add trait. Rust’s standard numeric types all implement std::ops::Add.

If you want to try writing out z.add(c), you’ll need to bring the Add trait into scope so that its method is visible:

1
2
3
4
use std::ops::Add;

assert_eq!(4.125f32.add(5.75), 9.875);
assert_eq!(10.add(20), 10 + 20);

Here’s the definition of std::ops::Add:

1
2
3
4
trait Add<Rhs = Self> {
    type Output;
    fn add(self, rhs: Rhs) -> Self::Output;
}

The trait Add<T> is the ability to add a T value to yourself. For example, if you want to be able to add i32 and u32 values to your type, your type must implement both Add<i32> and Add<u32>.

The trait’s type parameter Rhs defaults to Self, so if you’re implementing addition between two values of the same type, you can simply write Add for that case. The associated type Output describes the result of the addition.

For example, to be able to add Complex<i32> values together, Complex<i32> must implement Add<Complex<i32>>. Since we’re adding a type to itself, we just write Add:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
use std::ops::Add;

impl Add for Complex<i32> {
    type Output = Complex<i32>;
    fn add(self, rhs: Self) -> Self {
        Complex {
            re: self.re + rhs.re,
            im: self.im + rhs.im,
        }
    }
}

#[derive(Clone, Copy, Debug)]
struct Complex<T> {
    /// Real portion of the complex number
    re: T,

    /// Imaginary portion of the complex number
    im: T,
}

Of course, we shouldn’t have to implement Add separately for Complex<i32>, Complex<f32>, Complex<f64>, and so on. All the definitions would look exactly the same except for the types involved, so we should be able to write a single generic implementation that covers them all, as long as the type of the complex components themselves supports addition:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
impl<T> Add for Complex<T>
    where T: Add<Output = T>,
{
    type Output = Self;
    fn add(self, rhs: Self) -> Self {
        Complex {
            re: self.re + rhs.re,
            im: self.im + rhs.im,
        }
    }
}

By writing where T: Add<Output=T>, we restrict T to types that can be added to themselves, yielding another T value. This is a reasonable restriction, but we could loosen things still further: the Add trait doesn’t require both operands of + to have the same type, nor does it constrain the result type. So a maximally generic implementation would let the left- and righthand operands vary independently and produce a Complex value of whatever component type that addition produces:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
impl<L, R> Add<Complex<R>> for Complex<L>
    where L: Add<R>,
{
    type Output = Complex<L::Output>;
    fn add(self, rhs: Complex<R>) -> Self::Output {
        Complex {
            re: self.re + rhs.re,
            im: self.im + rhs.im,
        }
    }
}

In practice, however, Rust tends to avoid supporting mixed-type operations. Since our type parameter L must implement Add<R>, it usually follows that L and R are going to be the same type: there simply aren’t that many types available for L that implement anything else. So in the end, this maximally generic version may not be much more useful than the prior, simpler generic definition.

You can use the + operator to concatenate a String with a &str slice or another String. However, Rust does not permit the left operand of + to be a &str, to discourage building up long strings by repeatedly concatenating small pieces on the left.

  • This performs poorly, requiring time quadratic in the final length of the string.
  • Generally, the write! macro is better for building up strings piece by piece.

Rust’s built-in traits for arithmetic and bitwise operators come in 3 groups: unary operators, binary operators, and compound assignment operators. Within each group, the traits and their methods all have the same form.

Unary Operators

Aside from the dereferencing operator *, Rust has two unary operators that can be customized: - and !.

Trait nameExpressionEquivalent expression
std::ops::Neg-xx.neg()
std::ops::Not!xx.not()

All of Rust’s signed numeric types implement std::ops::Neg, for the unary negation operator -; the integer types and bool implement std::ops::Not, for the unary complement operator !. There are also implementations for references to those types.

! complements bool values and performs a bitwise complement (that is, flips the bits) when applied to integers; it plays the role of both the ! and ~ operators from C and C++.

These traits’ definitions are simple:

1
2
3
4
5
6
7
8
9
trait Neg {
    type Output;
    fn neg(self) -> Self::Output;
}

trait Not {
    type Output;
    fn not(self) -> Self::Output;
}

Negating a complex number simply negates each of its components. Here’s how we might write a generic implementation of negation for Complex values:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
use std::ops::Neg;
impl<T> Neg for Complex<T>
    where T: Neg<Output = T>,
{
    type Output = Complex<T>;
    fn neg(self) -> Complex<T> {
        Complex {
            re: -self.re,
            im: -self.im,
        }
    }
}

Binary Operators

CategoryTrait nameExpressionEquivalent expression
Arithmetic operatorsstd::ops::Addx + yx.add(y)
std::ops::Subx - yx.sub(y)
std::ops::Mulx * yx.mul(y)
std::ops::Divx / yx.div(y)
std::ops::Remx % yx.rem(y)
Bitwise operatorsstd::ops::BitAndx & yx.bitand(y)
std::ops::BitOrx | yx.bitor(y)
std::ops::BitXorx ^ yx.bitxor(y)
std::ops::Shlx << yx.shl(y)
std::ops::Shrx >> yx.shr(y)

All of Rust’s numeric types implement the arithmetic operators. Rust’s integer types and bool implement the bitwise operators. There are also implementations that accept references to those types as either or both operands.

You can use the + operator to concatenate a String with a &str slice or another String. However, Rust does not permit the left operand of + to be a &str, to discourage building up long strings by repeatedly concatenating small pieces on the left. (This performs poorly, requiring time quadratic in the final length of the string.) Generally, the write! macro is better for building up strings piece by piece.

Compound Assignment Operators

A compound assignment expression is one like x += y or x &= y: it takes two operands, performs some operation on them like addition or a bitwise AND, and stores the result back in the left operand. In Rust, the value of a compound assignment expression is always (), never the value stored.

Many languages have operators like these and usually define them as shorthand for expressions like x = x + y or x = x & y. In Rust, however, x += y is shorthand for the method call x.add_assign(y), where add_assign is the sole method of the std::ops::AddAssign trait:

1
2
3
trait AddAssign<Rhs = Self> {
    fn add_assign(&mut self, rhs: Rhs);
}

All of Rust’s numeric types implement the arithmetic compound assignment operators. Rust’s integer types and bool implement the bitwise compound assignment operators.

A generic implementation of AddAssign for our Complex type is straightforward:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
use std::ops::AddAssign;

impl<T> AddAssign for Complex<T>
where
    T: AddAssign<T>,
{
    fn add_assign(&mut self, rhs: Complex<T>) {
        self.re += rhs.re;
        self.im += rhs.im;
    }
}

The built-in trait for a compound assignment operator is completely independent of the built-in trait for the corresponding binary operator. Implementing std::ops::Add does not automatically implement std::ops::AddAssign.

Equivalence Comparisons

Rust’s equality operators, == and !=, are shorthand for calls to the std::cmp::PartialEq trait’s eq and ne methods:

1
2
assert_eq!(x == y, x.eq(&y));
assert_eq!(x != y, x.ne(&y));

Here’s the definition of std::cmp::PartialEq:

1
2
3
4
5
6
7
8
9
trait PartialEq<Rhs = Self>
where
    Rhs: ?Sized,
{
    fn eq(&self, other: &Rhs) -> bool;
    fn ne(&self, other: &Rhs) -> bool {
        !self.eq(other)
    }
}

Since the ne method has a default definition, you only need to define eq to implement the PartialEq trait, so here’s a complete implementation for Complex:

1
2
3
4
5
impl<T: PartialEq> PartialEq for Complex<T> {
    fn eq(&self, other: &Complex<T>) -> bool {
        self.re == other.re && self.im == other.im
    }
}

For any component type T that itself can be compared for equality, this implements comparison for Complex<T>.

Implementations of PartialEq are almost always of the form shown: they compare each field of the left operand to the corresponding field of the right. These get tedious to write, and equality is a common operation to support, so if you ask, Rust will generate an implementation of PartialEq for you automatically. Simply add PartialEq to the type definition’s derive attribute:

1
2
3
4
#[derive(Clone, Copy, Debug, PartialEq)]
struct Complex<T> {
    // ...
}

Rust’s automatically generated implementation is essentially identical to the handwritten code, comparing each field or element of the type in turn. Rust can derive PartialEq implementations for enum types as well. Naturally, each of the values the type holds (or might hold, in the case of an enum) must itself implement PartialEq.

Unlike the arithmetic and bitwise traits, which take their operands by value, PartialEq takes its operands by reference. This means that comparing non-Copy values like Strings, Vecs, or HashMaps doesn’t cause them to be moved:

1
2
3
4
5
6
let s = "d\x6fv\x65t\x61i\x6c".to_string();
let t = "\x64o\x76e\x74a\x69l".to_string();
assert!(s == t); // s and t are only borrowed...

// ... so they still have their values here.
assert_eq!(format!("{} {}", s, t), "dovetail dovetail");

where Rhs: ?Sized relaxes Rust’s usual requirement that type parameters must be sized types, letting us write traits like PartialEq<str> or PartialEq<[T]>. The eq and ne methods take parameters of type &Rhs, and comparing something with a &str or a &[T] is completely reasonable. Since str implements PartialEq<str>, the following assertions are equivalent:

1
2
assert!("ungula" != "ungulate");
assert!("ungula".ne("ungulate"));

Here, both Self and Rhs would be the unsized type str, making ne’s self and rhs parameters both &str values.

The traditional mathematical definition of an equivalence relation, of which equality is one instance, imposes three requirements. For any values x and y:

  1. If x == y is true, then y == x must be true as well. In other words, swapping the two sides of an equality comparison doesn’t affect the result.
  2. If x == y and y == z, then it must be the case that x == z.
    • Equality is contagious.
  3. It must always be true that x == x.

That last requirement might seem too obvious to be worth stating, but this is exactly where things go awry. Rust’s f32 and f64 are IEEE standard floating-point values. According to that standard, expressions like 0.0/0.0 and others with no appropriate value must produce special not-a-number values, usually referred to as NaN values. The standard further requires that a NaN value be treated as unequal to every other value—including itself. For example, the standard requires all the following behaviors:

1
2
3
assert!(f64::is_nan(0.0 / 0.0));
assert_eq!(0.0 / 0.0 == 0.0 / 0.0, false);
assert_eq!(0.0 / 0.0 != 0.0 / 0.0, true);

Furthermore, any ordered comparison with a NaN value must return false:

1
2
3
4
assert_eq!(0.0 / 0.0 < 0.0 / 0.0, false);
assert_eq!(0.0 / 0.0 > 0.0 / 0.0, false);
assert_eq!(0.0 / 0.0 <= 0.0 / 0.0, false);
assert_eq!(0.0 / 0.0 >= 0.0 / 0.0, false);

So while Rust’s == operator meets the first two requirements for equivalence relations, it clearly doesn’t meet the third when used on IEEE floating-point values. This is called a partial equivalence relation, so Rust uses the name PartialEq for the == operator’s built-in trait. If you write generic code with type parameters known only to be PartialEq, you may assume the first two requirements hold, but you should not assume that values always equal themselves.

If you’d prefer your generic code to require a full equivalence relation, you can instead use the std::cmp::Eq trait as a bound, which represents a full equivalence relation: if a type implements Eq, then x == x must be true for every value x of that type. In practice, almost every type that implements PartialEq should implement Eq as well; f32 and f64 are the only types in the standard library that are PartialEq but not Eq.

The standard library defines Eq as an extension of PartialEq, adding no new methods:

1
trait Eq: PartialEq<Self> {}

If your type is PartialEq and you would like it to be Eq as well, you must explicitly implement Eq, even though you need not actually define any new functions or types to do so. So implementing Eq for our Complex type is quick:

1
impl<T: Eq> Eq for Complex<T> {}

We could implement it even more succinctly by just including Eq in the derive attribute on the Complex type definition:

1
2
3
4
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
struct Complex<T> {
    // ...
}

Derived implementations on a generic type may depend on the type parameters. With the derive attribute, Complex<i32> would implement Eq, because i32 does, but Complex<f32> would only implement PartialEq, since f32 doesn’t implement Eq.

When you implement std::cmp::PartialEq yourself, Rust can’t check that your definitions for the eq and ne methods actually behave as required for partial or full equivalence. They could do anything you like. Rust simply takes your word that you’ve implemented equality in a way that meets the expectations of the trait’s users.

Although the definition of PartialEq provides a default definition for ne, you can provide your own implementation if you like. However, you must ensure that ne and eq are exact complements of each other. Users of the PartialEq trait will assume this is so.

Ordered Comparisons

Rust specifies the behavior of the ordered comparison operators <, >, <=, and >= all in terms of a single trait, std::cmp::PartialOrd:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
trait PartialOrd<Rhs = Self>: PartialEq<Rhs>
where
    Rhs: ?Sized,
{
    fn partial_cmp(&self, other: &Rhs) -> Option<Ordering>;

    fn lt(&self, other: &Rhs) -> bool {}
    fn le(&self, other: &Rhs) -> bool {}
    fn gt(&self, other: &Rhs) -> bool {}
    fn ge(&self, other: &Rhs) -> bool {}
}

PartialOrd<Rhs> extends PartialEq<Rhs>: you can do ordered comparisons only on types that you can also compare for equality.

The only method of PartialOrd you must implement yourself is partial_cmp. When partial_cmp returns Some(o), then o indicates self’s relationship to other:

1
2
3
4
5
enum Ordering {
    Less, // self < other
    Equal, // self == other
    Greater, // self > other
}

If partial_cmp returns None, that means self and other are unordered with respect to each other: neither is greater than the other, nor are they equal. Among all of Rust’s primitive types, only comparisons between floating-point values ever return None: specifically, comparing a NaN (not-a-number) value with anything else returns None.

Like the other binary operators, to compare values of two types Left and Right, Left must implement PartialOrd<Right>. Expressions like x < y or x >= y are shorthand for calls to PartialOrd methods:

ExpressionEquivalent method callDefault definition
x < yx.lt(y)x.partial_cmp(&y) == Some(Less)
x > yx.gt(y)x.partial_cmp(&y) == Some(Greater)
x <= yx.le(y)matches!(x.partial_cmp(&y), Some(Less) | Some(Equal)
x >= yx.ge(y)matches!(x.partial_cmp(&y), Some(Greater) | Some(Equal)

If you know that values of two types are always ordered with respect to each other, then you can implement the stricter std::cmp::Ord trait:

1
2
3
trait Ord: Eq + PartialOrd<Self> {
fn cmp(&self, other: &Self) -> Ordering;
}

The cmp method here simply returns an Ordering, instead of an Option<Ordering> like partial_cmp: cmp always declares its arguments equal or indicates their relative order. Almost all types that implement PartialOrd should also implement Ord. In the standard library, f32 and f64 are the only exceptions to this rule.

Since there’s no natural ordering on complex numbers, we can’t use our Complex type from the previous sections to show a sample implementation of PartialOrd. Instead, suppose you’re working with the following type, representing the set of numbers falling within a given half-open interval:

1
2
3
4
5
#[derive(Debug, PartialEq)]
struct Interval<T> {
    lower: T, // inclusive
    upper: T, // exclusive
}

One interval is less than another if it falls entirely before the other, with no overlap. If two unequal intervals overlap, they’re unordered: some element of each side is less than some element of the other. And two equal intervals are simply equal. The following implementation of PartialOrd implements those rules:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
use std::cmp::{Ordering, PartialOrd};

impl<T: PartialOrd> PartialOrd<Interval<T>> for Interval<T> {
    fn partial_cmp(&self, other: &Interval<T>) -> Option<Ordering> {
        if self == other {
            Some(Ordering::Equal)
        } else if self.lower >= other.upper {
            Some(Ordering::Greater)
        } else if self.upper <= other.lower {
            Some(Ordering::Less)
        } else {
            // unordered cases
            None
        }
    }
}

While PartialOrd is what you’ll usually see, total orderings defined with Ord are necessary in some cases, such as the sorting methods implemented in the standard library. For example, sorting intervals isn’t possible with only a PartialOrd implementation. If you do want to sort them, you’ll have to fill in the gaps of the unordered cases. You might want to sort by upper bound, for instance, and it’s easy to do that with sort_by_key:

1
intervals.sort_by_key(|i| i.upper);

The Reverse wrapper type takes advantage of this by implementing Ord with a method that simply inverts any ordering. For any type T that implements Ord, std::cmp::Reverse<T> implements Ord too, but with reversed ordering. For example, sorting our intervals from high to low by lower bound is simple:

1
2
use std::cmp::Reverse;
intervals.sort_by_key(|i| Reverse(i.lower));

Index and IndexMut

You can specify how an indexing expression like a[i] works on your type by implementing the std::ops::Index and std::ops::IndexMut traits. Arrays support the [] operator directly, but on any other type, the expression a[i] is normally shorthand for *a.index(i), where index is a method of the std::ops::Index trait. However, if the expression is being assigned to or borrowed mutably, it’s instead shorthand for *a.index_mut(i), a call to the method of the std::ops::IndexMut trait.

Here are the traits’ definitions:

1
2
3
4
5
6
7
8
trait Index<Idx> {
    type Output: ?Sized;
    fn index(&self, index: Idx) -> &Self::Output;
}

trait IndexMut<Idx>: Index<Idx> {
    fn index_mut(&mut self, index: Idx) -> &mut Self::Output;
}

These traits take the type of the index expression as a parameter (type parameter). You can index a slice with a single usize, referring to a single element, because slices implement Index<usize>. But you can refer to a subslice with an expression like a[i..j] because they also implement Index<Range<usize>>. That expression is shorthand for:

1
*a.index(std::ops::Range { start: i, end: j })

Rust’s HashMap and BTreeMap collections let you use any hashable or ordered type as the index. The following code works because HashMap<&str, i32> implements Index<&str>:

1
2
3
4
5
6
7
8
9
use std::collections::HashMap;
let mut m = HashMap::new();
m.insert("十", 10);
m.insert("百", 100);
m.insert("千", 1000);
m.insert("万", 1_0000);

assert_eq!(m["十"], 10);
assert_eq!(m["千"], 1000);

Those indexing expressions are equivalent to:

1
2
3
use std::ops::Index;
assert_eq!(*m.index("十"), 10);
assert_eq!(*m.index("千"), 1000);

The Index trait’s associated type Output specifies what type an indexing expression produces: for our HashMap, the Index implementation’s Output type is i32.

The IndexMut trait extends Index with an index_mut method that takes a mutable reference to self, and returns a mutable reference to an Output value. Rust automatically selects index_mut when the indexing expression occurs in a context where it’s necessary:

1
2
3
4
let mut desserts =
    vec!["Howalon".to_string(), "Soan papdi".to_string()];
desserts[0].push_str(" (fictional)");
desserts[1].push_str(" (real)");

Because the push_str method operates on &mut self, those last two lines are equivalent to:

1
2
3
use std::ops::IndexMut;
(*desserts.index_mut(0)).push_str(" (fictional)");
(*desserts.index_mut(1)).push_str(" (real)");

One limitation of IndexMut is that, by design, it must return a mutable reference to some value. This is why you can’t use an expression like m["十"] = 10; to insert a value into the HashMap m: the table would need to create an entry for "十" first, with some default value, and return a mutable reference to that. But not all types have cheap default values, and some may be expensive to drop; it would be a waste to create such a value only to be immediately dropped by the assignment.

Other Operators

Not all operators can be overloaded in Rust. As of Rust 1.56, the error-checking  ? operator works only with Result and a few other standard library types, but work is in progress to expand this to user-defined types as well. Similarly, the logical operators && and || are limited to Boolean values only. The .. and ..= operators always create a struct representing the range’s bounds, the & operator always borrows references, and the = operator always moves or copies values. None of them can be overloaded.

The dereferencing operator, *val, and the dot operator for accessing fields and calling methods, as in val.field and val.method(), can be overloaded using the Deref and DerefMut traits.

Rust does not support overloading the function call operator, f(x). Instead, when you need a callable value, you’ll typically just write a closure.


References