---
title: 'Trait Bounds'
source: 'https://academia.sh/en/courses/rust-abstraction/trait-bounds'
course: 'Traits, Generics and Error Handling'
language: en
updated: '2026-09-15T04:58:42+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Defines the trait bound syntax (single, multiple, and the where clause) and explains its purpose'
    - 'Compares the run count of a bounded generic body against a concrete-typed body doing the same job to show that the bound adds no step to the run'
    - 'States that a cost exists that cannot be measured in the run (a separate body generated per type) and avoids claiming it with a number'
---

# Trait Bounds

Putting a trait bound on a generic function's type parameter adds no step to the source at all — the same body written and run in both a bounded and an unbounded form gives back the exact same number in both; the bound's cost is written to the compilation, not to the run ledger, and it cannot be measured by this number.

The previous lesson showed that a trait can carry a default body and that which type this body
runs for can be counted in the run. But the `<T: Label>` syntax of a function like
`static_dispatch` has not been explained yet — this is a **trait bound** placed on a generic
function's type parameter, and it is this lesson's subject. The question stays the same: how
many times did this body you wrote in the source run? But this time what's measured is not the
body — it is the **condition** placed around the body.

## What the Bound Says

A generic function written as `fn name<T>(x: &T)` can accept any type; but if the body wants
to call a method on `x`, the compiler needs to know that method exists. A trait bound provides
exactly this: the syntax `fn name<T: Label>(x: &T)` says "whatever `T` is, it must implement
`Label`." Without the bound, a call like `x.ledger()` does not compile, because the compiler
does not know which methods exist for `T`; once the bound is added, the compiler reads this
information from the trait declaration.

An unbounded generic function means the compiler knows nothing about `T`. When `fn name<T>(x:
&T)` is written, the compiler only allows operations common to every type on `x` (moving it,
taking a reference) — if a call specific to `Label`, like `x.ledger()`, is written, the
compiler rejects it, because all it knows is that `T` *is a type*, not that it implements
`Label`. Unlike the type ledger in the previous course, this rejection is not a run-time
behavior here — the source does not compile at all. A trait bound fills exactly this gap: from
the moment `T: Label` is written, the compiler knows that every method `Label` declares can be
called on `x` inside the body, and `x.ledger()` becomes something that compiles. The bound thus
binds both sides at once: the calling side ("only types that implement `Label` can be passed to
this function") and the body side ("this body can safely call `Label`'s methods").

The same bound can be written in two different places. The short form goes right next to the
type parameter: `fn bounded_body<T: Label>(x: &T)`. When the bound gets long (multiple traits,
long type names), the same meaning can also be written with a `where` clause:

```rust
// where-form.rs — the same bound, written apart from the signature.
trait Label {
    fn name(&self) -> String;
}

struct Example;
impl Label for Example {
    fn name(&self) -> String {
        String::from("example")
    }
}

fn short<T: Label>(x: &T) -> String {
    x.name()
}

fn long<T>(x: &T) -> String
where
    T: Label,
{
    x.name()
}

fn main() {
    let e = Example;
    println!("short={} long={}", short(&e), long(&e));
}
```

```
short=example long=example
```

**TG3.** `short` and `long` mean exactly the same thing to the compiler; the `where` clause is
a readability option, not a separate mechanism. When more than one bound is needed (something
like `T: Label + Clone`), the short form makes the line longer, while the `where` form can keep
the signature short by putting each bound on its own line. This lesson uses the two
interchangeably; whichever is chosen has no effect at all on the run count.

Multiple bounds are joined with the `+` sign: `T: Label + Clone` means "`T` must implement both
`Label` and `Clone`." This is a familiar structure from the previous lesson: `Label` alone was
a single contract; here two contracts are bound to the same type parameter at once. `+`
establishes an **intersection** here, not a union: the set of types that can stand in for `T`
is the intersection of the set of types implementing only `Label` and the set of types
implementing only `Clone`. Adding a bound always **narrows** which types can be passed to a
function, never widens it — so adding a new bound to a signature can retroactively cut off
some calls, but it can never make a new call possible.

Bounds also do not change *where* the type parameter is constrained: the only difference between
`<T: Label>` and `where T: Label` is placement, not the strength of the constraint. The same
principle holds when two bounds are written together (`T: Label + Clone`): the compiler, while
checking the body, assumes `T` carries the methods of *both* `Label` *and* `Clone`; if either
bound were missing, a single call belonging to that trait would already make the body fail to
compile.

A signature can carry more than one type parameter, and each parameter can have its own bound:
in `fn combine<T: Label, U: Label>(a: &T, b: &U)`, `T` and `U` look like they share the same
bound, but to the compiler they are two independent parameters — `a` and `b` can be different
concrete types, the only shared requirement is that both implement `Label`. Bounds are placed
per parameter; one parameter's bound does not affect another, even if the same trait name is
repeated in both places.

```rust
// combine.rs — two type parameters, each independently carrying the same trait bound.
trait Label {
    fn name(&self) -> String;
}

struct Plain;
struct Custom;

impl Label for Plain {
    fn name(&self) -> String {
        String::from("plain")
    }
}
impl Label for Custom {
    fn name(&self) -> String {
        String::from("custom")
    }
}

fn combine<T: Label, U: Label>(a: &T, b: &U) -> String {
    format!("{}+{}", a.name(), b.name())
}

fn main() {
    let p = Plain;
    let c = Custom;
    println!("{}", combine(&p, &c));
    println!("{}", combine(&p, &p));
}
```

```
plain+custom
plain+plain
```

In the second call, `T` and `U` both resolve to the same concrete type (`Plain`); in the
first, they resolve to two different concrete types (`Plain` and `Custom`). `combine`'s
signature accepts both calls, because the bound only says "must implement `Label`," not "must
be the same type." If we wanted to force `T` and `U` to resolve to the same concrete type, the
signature would use a single parameter instead (`fn combine<T: Label>(a: &T, b: &T)`); having
two separate parameters exists precisely to **remove** that requirement.

When no trait bound is written, a type parameter is not left completely empty: every `T`, unless
stated otherwise, also carries an implicit **size** bound — the compiler assumes `T` has a
fixed, known size in memory, because a generic body needs to be able to hold a value of type
`T` locally. This implicit bound is a separate layer from explicitly written bounds like
`Label`, and it is invisible for most generic functions; it only needs to be explicitly relaxed
in a small number of advanced writings that work with types of variable size, like slices.
This lesson does not use that relaxation — what matters here is knowing that alongside the bound
that is **visible** (`Label`), there is always an invisible default bound as well.

## The Bound's Contribution to the Run: Zero Steps

The same job can be set up in two forms to compare the run count: one generic with a
`<T: Label>` bound, the other a direct concrete type (`Plain`), an unbounded function. A third
function uses more than one bound (`Label + Clone`) together.

```rust
// times.rs — the slice of trait Label: `ledger`, the type name, and two implementations.

use std::cell::RefCell;
use std::collections::BTreeMap;
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)
}

pub trait Label {
    fn ledger(&self) -> &Ledger;
    fn kind(&self) -> &'static str;
}

#[derive(Clone)]
pub struct Plain {
    pub ledger: Ledger,
}

impl Label for Plain {
    fn ledger(&self) -> &Ledger {
        &self.ledger
    }
    fn kind(&self) -> &'static str {
        "plain"
    }
}

pub struct Custom {
    pub ledger: Ledger,
}

impl Label for Custom {
    fn ledger(&self) -> &Ledger {
        &self.ledger
    }
    fn kind(&self) -> &'static str {
        "custom"
    }
}
```

```rust
// bound-body.rs — measures that a trait bound adds no step at all to the run.
mod times;
use times::{times, record, Ledger, Label, Custom, Plain};
use std::cell::RefCell;
use std::collections::BTreeMap;
use std::rc::Rc;

// The same source line is generated separately by the compiler for every
// concrete type it is called with — this is a cost the run counter does not see.
fn bounded_body<T: Label>(x: &T) -> u32 {
    let key = format!("bounded-body-{}", x.kind());
    record(x.ledger(), &key);
    times(x.ledger(), &key)
}

// The same job, unbounded: takes a concrete type directly, no type parameter.
fn concrete_body(x: &Plain) -> u32 {
    record(x.ledger(), "concrete-body");
    times(x.ledger(), "concrete-body")
}

// T must provide both Label and Clone — two bounds joined with '+'.
fn clone_and_record<T: Label + Clone>(x: &T) -> T {
    record(x.ledger(), "multi-bound-body");
    x.clone()
}

fn main() {
    let ledger: Ledger = Rc::new(RefCell::new(BTreeMap::new()));
    let plain = Plain { ledger: Rc::clone(&ledger) };
    let custom = Custom { ledger: Rc::clone(&ledger) };

    let _ = bounded_body(&plain);
    let _ = bounded_body(&plain);
    let _ = bounded_body(&custom);

    let _ = concrete_body(&plain);
    let _ = concrete_body(&plain);

    let clone = clone_and_record(&plain);
    let _ = clone_and_record(&clone);

    let mut bodies: BTreeMap<&str, u32> = BTreeMap::new();
    bodies.insert("bounded-body-custom", times(&ledger, "bounded-body-custom"));
    bodies.insert("bounded-body-plain", times(&ledger, "bounded-body-plain"));
    bodies.insert("concrete-body", times(&ledger, "concrete-body"));
    bodies.insert("multi-bound-body", times(&ledger, "multi-bound-body"));

    println!("{:<22}{}", "body", "how many times");
    for (name, n) in &bodies {
        println!("{:<22}{}", name, n);
    }
}
```

```
body                  how many times
bounded-body-custom   1
bounded-body-plain    2
concrete-body         2
multi-bound-body      2
```

**TG4.** `bounded-body-plain` ran twice, `concrete-body` also ran twice — the exact same
number. Both do the same job (reading the ledger and counting one row); one through a generic
type parameter, the other directly through a concrete type. The trait bound adds or subtracts
nothing from **how many times** the body is called — this shows that the bound leaves no trace
at all in the run ledger. A bound is a **compile-time** rule: it decides which types can be
passed to `bounded_body`, but once passed, the running code behaves as if the bound never
existed.

The Interfaces, Errors and Generics course measured, for Go's generics, that **the binding was
established at the call site** — a generic function's type constraint restricted which types a
call could be made with, at the call site. This course is not asking that same question; what's
measured here is **what the bound adds to the run** — and the answer is nothing.
`bounded-body-custom` confirms this from a second angle: the same `bounded_body` function can
also be called with `Custom` (once), because `Custom` also implements `Label`; the bound
decides which types are accepted, but the body runs the same way for every accepted type.

`multi-bound-body` falls into the same class too: called twice, ran twice. Here the
`T: Label + Clone` bound combines two separate conditions, but the body itself is still a
single record call plus a single clone operation. As the number of bounds grows (`Label`, then
`Label + Clone`), the line count in the run ledger does not grow; a bound is a condition placed
on the type parameter, not a step added inside the body.

All four rows fall into the **every time** class: none of them is "only when needed" or
"never," because every call unavoidably finds its match — there is no skipped, deferred, or
unconsumed step. This is the same class as `default-body-plain` and `overridden-body` from the
previous lesson: the only difference is which condition the body can be called under, not
whether the body itself runs.

This equality will come up again under a different question later in the course. The Object
Safety and Dynamic Dispatch lesson will compare the same `Label` contract, this time using
`&dyn Label` instead of the `<T: Label>` bound, against a call form. There too the count comes
out equal — but for a different reason than what's seen here: bounded generics resolve the
call at compile time, while the `dyn` form finds the same call at run time from a method table.
This lesson only shows that the bound's **existence** adds nothing to the run; how the two call
forms differ from each other is taken up in a separate lesson.

`concrete_body` not needing a bound at all is also a consequence of the same principle. When a
function is already bound to a single concrete type (`Plain`), the answer to "which types
should be accepted" is already known from the start — only `Plain`. A trait bound exists for
cases where the answer to this question can be **more than one** type; there is no need to even
ask it in a function bound to a single type. So the difference between `concrete_body` and
`bounded_body` is not a shortcoming — it is the answer to two different questions: one never asks
"which type is right for this job," the other answers that question with a bound.

## The Measurement's Limit

Having no cost in the run does not mean the bound is **free**. When the `bounded_body` function
is called separately for `Plain` and `Custom`, the compiler works by a method that generates a
**separate body for every concrete type**: there is a single `fn bounded_body<T: Label>` line in
the source, but the resulting binary has one body for `Plain` and another for `Custom`. This
generation happens during compilation and never enters the run ledger — the ledger only counts
calls at run time, not how many bodies were generated at compile time. This course's
measurement method **cannot show** this cost; the lesson says so openly and does not try to count
it. `bounded-body-plain` and `bounded-body-custom` running three times in total carries no
information at all about how many separate bodies were generated behind the scenes.

This limit also exposes a general limit of the course's measurement method: the run ledger can
only count lines that are **actually passed at run time**. Nothing that happens at compile
time — how many separate bodies were generated, in what order each type was compiled, how many
candidate types a bound eliminated — can be made visible with this ledger. Throughout the
course, the word "count" always means a **run-time count**; when a question belongs to compile
time, the lesson says so openly and answers it in prose, instead of making up a number. This
distinction matters for setting out from the start what the measurement does and does not cover:
for the rest of the course, the sentence "this body never ran" is always an observation that
belongs to run time, not a claim about whether the source exists.

## Summary

- A trait bound places the condition "must implement this trait" on a generic function's type
  parameter; the short form (`<T: Label>`) and the `where` clause mean the same thing to the
  compiler.
- Multiple bounds are joined with `+` (`T: Label + Clone`); the body stays a single one, running
  code does not grow as the bound count grows.
- A bounded body (`bounded-body-plain`) and an unbounded body doing the same job
  (`concrete-body`) ran the exact same number of times every time: two each — the bound adds no
  step at all to the run ledger.
- The bound's real cost is not in the run but in the compilation: a separate body is generated
  for every concrete type, but this generation cannot be measured by the run counter, and the
  lesson does not try to.

## Next Step

The bound tied a trait to the type parameter from the outside. A trait can sometimes want to
carry a type **within** the contract itself — a type the caller never states, that the
implementer decides. The next lesson compares this associated type against writing the same
contract with a type parameter instead.
