---
title: 'Operator Overloading'
source: 'https://academia.sh/en/courses/rust-abstraction/operator-overloading'
course: 'Traits, Generics and Error Handling'
language: en
updated: '2026-09-15T04:58:42+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Shows that using an operator corresponds to a method call on a trait in the standard library'
    - 'Verifies by running it that in a chained operator expression the call count is one less than the term count'
    - "Explains that the operator's call site is not visible in the source, so the call count can only be known by counting"
---

# Operator Overloading

Every use of an operator (like +) is a trait call hiding behind it; a three-term sum calls it twice, a five-term sum four times — the operator's short syntax does not change the call count, it only changes whether the call is visible in the source, and this number can only be known from the run ledger.

The previous lesson showed that a body with no line written in the source still runs. This
lesson looks at the opposite kind of invisibility: the body is written in the source, but the
line that **calls** it is not. When `a + b` is written, a call is made, but there is no line
carrying the name `add` anywhere in the source — the call is hidden behind the `+` operator. The
Object-Oriented Java course already introduced operator overloading; what was measured there was
which type made the decision, what's measured here is how many times an operator's body
**runs**.

Every body measured across this course's five lessons could be referred to by a name in the
source: `name`, a bounded function, `produce`, `static_dispatch`/`dynamic_dispatch`, `eq`. In
operator overloading this naming disappears — only `+` appears in the source, the word `add`
never appears anywhere. This final lesson carries the course's measurement axis to its most
invisible application: to count how many times a body ran, you first have to **recognize** the
line that calls it in the source, and with operators this recognition step lies not in the
source itself, but in the language's syntax rule.

## An Operator Is a Trait Call

In Rust, the `+` operator is not a fixed behavior, it is a call on the `std::ops::Add` trait. If a
type wants to be addable with `+`, it has to implement `Add`; if it does not, `+` does not
compile. The `a + b` syntax is **expanded** by the compiler into `Add::add(a, b)` — the shortcut
visible in the source carries an ordinary method call behind it.

This expansion is not in the same category as the derive from the previous lesson. A derived
body was never written in the source at all; the `add` body here is fully written, sitting line
by line inside the `impl Add for Length` block. What's invisible is not the body, it is the line
that **calls** the body — the `+` sign does not say it carries a call, it just places two values
side by side. This is the same even for the primitive integer and floating-point types: when
`3 + 4` is written, the same pattern runs under the hood, only for a primitive type like `i32`
this call is resolved through the compiler's own built-in rules, via the standard library's
`Add` implementation. For a user-defined type like `Length`, opening the same path means
implementing `Add` exactly.

```rust
// times.rs — slice: the record/times ledger, Length and the Add implementation.

use std::cell::RefCell;
use std::collections::BTreeMap;
use std::ops::Add;
use std::rc::Rc;

pub type Ledger = Rc<RefCell<BTreeMap<String, u32>>>;

pub fn record(ledger: &Ledger, name: &str) {
    *ledger.borrow_mut().entry(name.to_string()).or_insert(0) += 1;
}

pub fn times(ledger: &Ledger, name: &str) -> u32 {
    *ledger.borrow().get(name).unwrap_or(&0)
}

#[derive(Clone)]
pub struct Length {
    pub value: i32,
    pub ledger: Ledger,
}

impl Add for Length {
    type Output = Length;
    fn add(self, other: Length) -> Length {
        record(&self.ledger, "operator-body");
        Length {
            value: self.value + other.value,
            ledger: Rc::clone(&self.ledger),
        }
    }
}
```

The `impl Add for Length` block carries a notation familiar from the previous lesson: the line
`type Output = Length;` is the associated type covered in the Associated Types lesson — `Add`
itself carries the sum's result type within itself as a type. This lesson does not look at
`Output`, it looks at how many times the `add` body is called.

The `add` method's signature carries an observation of its own: `fn add(self, other: Length) ->
Length`. The parameter takes `self` itself (not `&self`) — the addition operation **consumes**
the two `Length` values, it does not borrow them. This is the ownership and move rule built in the
Ownership and the Memory Model course showing up again here: once `a + b` is written, neither `a`
nor `b` can be used after that expression, because both were moved into `add`. This is exactly
why `a` is cloned first in the `a.clone() + b` syntax below — since `a` will be used again in the
second sum (`sum + a`), it must not be consumed in the first sum.

## Three Terms, Two Calls

```rust
// operator.rs — every use of an operator is a call; a three-term sum calls it twice.
mod times;
use times::{times, Ledger, Length};
use std::cell::RefCell;
use std::collections::BTreeMap;
use std::rc::Rc;

fn build(ledger: &Ledger, value: i32) -> Length {
    Length { value, ledger: Rc::clone(ledger) }
}

fn main() {
    let ledger: Ledger = Rc::new(RefCell::new(BTreeMap::new()));
    let a = build(&ledger, 3);
    let b = build(&ledger, 4);

    // Three terms, two '+' calls: (a.clone() + b) + a
    let sum = a.clone() + b;
    let sum = sum + a;
    println!(
        "three-term-sum={} operator-body={}",
        sum.value,
        times(&ledger, "operator-body")
    );

    // Five terms, four '+' calls - the same principle verified with a separate ledger.
    let ledger2: Ledger = Rc::new(RefCell::new(BTreeMap::new()));
    let five: Vec<Length> = (1..=5).map(|d| build(&ledger2, d)).collect();
    let mut running_total = five[0].clone();
    for term in five.into_iter().skip(1) {
        running_total = running_total + term;
    }
    println!(
        "five-term-sum={} operator-body={}",
        running_total.value,
        times(&ledger2, "operator-body")
    );
}
```

```
three-term-sum=10 operator-body=2
five-term-sum=15 operator-body=4
```

**TG12.** The first measurement sums three terms (a clone of `a`, `b`, `a` itself), and
`operator-body` ran twice. The `+` sign also appeared twice in the source — `a.clone() + b` and
adding the result to `a` — and this appearance lines up exactly with how many times the body was
called. This row falls into the **every time** class: every use of the operator unavoidably
corresponds to a call. The second measurement confirms the same principle with a separate
ledger: when five terms are summed, the `+` written once in the source ran four times. The two
measurements together show a general rule: **a sum of n terms runs the operator body n minus one
times** — the first term is already in hand without needing any call, every remaining term
requires one call.

This rule comes from the structure of addition itself: summing two terms is one call, adding a
third term is a second call, adding a fourth is a third call. In the five-term loop this
accumulation starts from `five[0]` — zero calls so far, since no addition has happened yet — and
every new term adds one call, completing at four calls. The loop body in the source is a single
line (`running_total = running_total + term;`), but what matters is not how many times this line
was **written**, it is how many times it **ran** — a line written once in the source repeats
itself four times through the loop. This is an extra observation carried by the Operator
Overloading lesson: how many times a call is **written** in the source and how many times it
**runs** at run time can be different numbers, and a loop is the most common tool that pulls
these two apart.

The Object-Oriented Java course measured operator overloading's **which type made the
decision** — whether the declared type or the runtime type of the operands decides which
implementation is called for an operator. This course is not asking that question again; what's
measured here is how many times an already-determined implementation's body **runs**. In Rust
this determination already happens at compile time, read from `Length`'s concrete type — the
determination itself is not this lesson's subject.

## The Syntax Is a Shortcut, the Call Is the Same

That the `+` syntax is a shortcut can also be verified by calling `Add::add` explicitly — if the
two notations expand to the same call, the trace both leave in the run ledger should be the
same.

```rust
// operator-equivalent.rs — the '+' syntax and the Add::add call run the same number of times.
mod times;
use times::{times, Ledger, Length};
use std::cell::RefCell;
use std::collections::BTreeMap;
use std::ops::Add;
use std::rc::Rc;

fn build(ledger: &Ledger, value: i32) -> Length {
    Length { value, ledger: Rc::clone(ledger) }
}

fn main() {
    let ledger: Ledger = Rc::new(RefCell::new(BTreeMap::new()));
    let a = build(&ledger, 5);
    let b = build(&ledger, 6);

    // Two notations, the same call: one carrying '+', the other calling Add::add explicitly.
    let via_syntax = a.clone() + b.clone();
    let explicit = Add::add(a, b);

    println!("via_syntax={} explicit={}", via_syntax.value, explicit.value);
    println!("operator-body={}", times(&ledger, "operator-body"));
}
```

```
via_syntax=11 explicit=11
operator-body=2
```

**TG13.** `a.clone() + b.clone()` is one call, `Add::add(a, b)` is one call — both trigger the
same `add` body, and `operator-body` runs twice. This confirms that the `+` sign does not add any
new behavior on the compiler's part, it is only a way of shortening a call that already exists.
`Add::add(a, b)` carries the trait's name and method explicitly; `a + b` carries the same call
but hides the name. Choosing between the two has no effect at all on the run count, it only
affects the **readability** of the source.

`Add` is not alone; the `std::ops` module carries a similar trait for every binary operator —
`Sub` for subtraction, `Mul` for multiplication, `Index` for indexing access. All rest on the
same principle: a sign expands into a trait call. This lesson only measured `Add`, but the
counting method stays the same for all of them: every use is a call, and when chained, the call
count is one less than the term count.

This generalization is not an assumption — it comes from the same pattern being repeated across
the other traits, just like `Add`. `Sub` declares a single method named `sub`, `Mul` one named
`mul`, `Index` one named `index`, and each hides the same way behind its own sign (`-`, `*`,
`[]`). A type can also overload more than one operator — `Length` could have implemented both
`Add` and `Sub` if it wanted, each sitting in an independent `impl` block, and neither would
affect the other's run count. This lesson focused only on `Add` because the course's measurement
kernel already carried it; the principle stays the same for the remaining operators.

## The Measurement's Limit

An operator body is an ordinary method, but **where it is called is invisible in the source**.
In `a.clone() + b`, the word `add` does not appear anywhere — the call hides behind the `+` sign.
A reader can answer "how many calls are in this expression" by counting the signs: every `+` is
a call. But as a chain grows longer, especially when produced inside a loop or an iterator
chain, counting the `+` signs in the source alone may not be enough either — how many times the
loop ran is also information you need to know. In the five-term loop above, only one `+` is
written in the source (`running_total = running_total + term;`), but since this line runs four
times inside the loop, the total call count is four. The number of signs in the source and the
run-time call count diverge here: **how many times it ran can only be known by counting, only by
the run ledger** — not by reading the source.

This is the final form of the question this course has asked from the start: how many times did
this body you wrote in the source run? In operator overloading this question shows up in its
most invisible form, because the call itself carries no name — only a sign. Of the sixteen
bodies measured across the course, the last one also stood behind this sign.

This invisibility confirms, from the other direction, an observation from the course's first
lesson. The Rust Design Goals lesson had shown that `HashSet::insert` and `Vec::push` do the
same job (adding an element) but fall into different classes when you look at their signatures —
the class could be determined by **reading** the source. In operator overloading, the class
(every time) can still be read from the source too — `+` is always a call — but the **count**
cannot be read from the source, because every `+` sign in the source can run more than once if
it is inside a loop. Throughout the course, the distinction "class is read from the source, count
comes from the run" is seen here in its clearest form.

These six lessons asked the same question in six different settings. Trait Definition and
Implementation showed that a default body written in the source runs for one type and never for
another. Trait Bounds showed that a condition placed around that body does not touch the run
count at all. Associated Types showed that two different notations of the same contract leave
the run count equal, the difference being in what the caller writes. Object Safety and Dynamic
Dispatch showed that whether the call is resolved at compile time or at run time does not change
the count either. Derived Traits showed that a body can run without ever being written in the
source, but that run cannot be counted. And this lesson showed that a written body's call can be
invisible in the source. Together, all six show that the question "how many times did it run" is
independent of visibility in the source: sometimes a visible body does not run, sometimes an
invisible call does.

## Summary

- Using an operator (like `+`) corresponds to a method call (`Add`) in the standard library; the
  shortcut visible in the source carries an ordinary call behind it.
- The operator body (`operator-body`) falls into the every-time class: it runs twice in a
  three-term sum, four times in a five-term sum — the call count is always one less than the
  term count.
- The operator's syntactic brevity does not change the call count; calling `Length::add` by hand
  runs the same number of times as writing `+`.
- Since the call site is not visible in the source (only a sign is visible), how many times it
  ran can only be known by counting with the run ledger, not by reading the source.

This general rule — n terms, n minus one calls — holds not just for `Add`, but for every
notation that chains binary operators. If the chain's length is written once in the source and
repeated with a loop (as in the five-term example above), the only way to know the call count
in advance is knowing how many times the loop will run; counting the operator signs in the
source alone is not enough. This is the final example of a distinction the course has carried
from the start: the source says **what** will run, the run says **how many times** it ran, and
the two do not carry the same information.

## Next Step

This course measured traits, bounds, associated types, dispatch forms, derived bodies, and
operator calls by asking when a body runs — every time, only when needed, or never. The next
topic carries the same question to places where a body can **fail**: how is a value's absence
expressed in the type, and does a body that handles that absence run every time, or only when
needed?
