Operator Overloading in Rust
Contents
You can make your own types support arithmetic and other operators, too, just by implementing a few built-in traits. This is called operator overloading.
#[derive(Clone, Copy, Debug)]
struct Complex<T> {
/// Real portion of the complex number
re: T,
/// Imaginary portion of the complex number
im: T,
}
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
.
use std::ops::Add;
assert_eq!(4.125f32.add(5.75), 9.875);
assert_eq!(10.add(20), 10 + 20);
- Bring the
Add
trait into scope so that its method is visible.
The definition of std::ops::Add
:
trait Add<Rhs = Self> {
type Output;
fn add(self, rhs: Rhs) -> Self::Output;
}
-
The trait
Add<T>
is the ability to add aT
value to yourself.- If you want to be able to add
i32
andu32
values to your type, your type must implement bothAdd<i32>
andAdd<u32>
.
- If you want to be able to add
-
The trait’s type parameter
Rhs
defaults toSelf
, so if you’re implementing addition between two values of the same type, you can simply writeAdd
for that case.use std::ops::Add; // impl Add<Complex<i32>> for Complex<i32> { 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, } } }
-
We shouldn’t have to implement
Add
separately forComplex<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: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 restrictT
to types that can be added to themselves, yielding anotherT
value.
- By writing
-
A maximally generic implementation would let the left- and right-hand operands vary independently and produce a Complex value of whatever component type that addition produces:
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.
-
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.
Unary Operators
Aside from the dereferencing operator *
, Rust has two unary operators that can be customized: -
and !
.
Trait name | Expression | Equivalent expression |
---|---|---|
std::ops::Neg |
-x |
x.neg() |
std::ops::Not |
!x |
x.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.
!
complementsbool
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++.
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.
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
Category | Trait name | Expression | Equivalent expression |
---|---|---|---|
Arithmetic operators | std::ops::Add |
x + y |
x.add(y) |
std::ops::Sub |
x - y |
x.sub(y) |
|
std::ops::Mul |
x * y |
x.mul(y) |
|
std::ops::Div |
x / y |
x.div(y) |
|
std::ops::Rem |
x % y |
x.rem(y) |
|
Bitwise operators | std::ops::BitAnd |
x & y |
x.bitand(y) |
std::ops::BitOr |
x | y |
x.bitor(y) |
|
std::ops::BitXor |
x ^ y |
x.bitxor(y) |
|
std::ops::Shl |
x << y |
x.shl(y) |
|
std::ops::Shr |
x >> y |
x.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.
Compound Assignment Operators
A compound assignment expression is one like x += y
or x &= y
. 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:
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.
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:
assert_eq!(x == y, x.eq(&y));
assert_eq!(x != y, x.ne(&y));
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:
impl<T: PartialEq> PartialEq for Complex<T> {
fn eq(&self, other: &Complex<T>) -> bool {
self.re == other.re && self.im == other.im
}
}
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:
#[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 String
s, Vec
s, or HashMap
s doesn’t cause them to be moved, which would be troublesome:
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 traditional mathematical definition of an equivalence relation, of which equality is one instance, imposes three requirements. For any values x
and y
:
- If
x == y
is true, theny == x
must be true as well. - If
x == y
andy == z
, then it must be the case thatx == z
.- Equality is contagious.
- It must always be true that
x == x
.
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. Any ordered comparison with a NaN value must return 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 implementEq
as well;f32
andf64
are the only types in the standard library that arePartialEq
but notEq
.
The standard library defines Eq as an extension of PartialEq, adding no new methods:
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. We could implement it even more succinctly by just including Eq
in the derive attribute on the Complex
type definition.
impl<T: Eq> Eq for Complex<T> {}
#[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
:
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>
extendsPartialEq<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 ispartial_cmp
.-
When
partial_cmp
returnsSome(o)
, theno
indicates self’s relationship to other:enum Ordering { Less, // self < other Equal, // self == other Greater, // self > other }
-
If
partial_cmp
returnsNone
, that meansself
andother
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 returnNone
: specifically, comparing a NaN (not-a-number) value with anything else returnsNone
.
-
Expression | Equivalent method call | Default definition |
---|---|---|
x < y |
x.lt(y) |
x.partial_cmp(&y) == Some(Less) |
x > y |
x.gt(y) |
x.partial_cmp(&y) == Some(Greater) |
x <= y |
x.le(y) |
matches!(x.partial_cmp(&y), Some(Less) | Some(Equal) |
x >= y |
x.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:
trait Ord: Eq + PartialOrd<Self> {
fn cmp(&self, other: &Self) -> Ordering;
}
- The cmp method here simply returns an
Ordering
, instead of anOption<Ordering>
likepartial_cmp
:cmp
always declares its arguments equal or indicates their relative order. - Almost all types that implement
PartialOrd
should also implementOrd
. In the standard library,f32
andf64
are the only exceptions to this rule.
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
.
Other Operators
Not all operators can be overloaded in Rust.
References