Default Trait Implementations
A Rust trait can give one of its methods a real body right in the trait definition, instead of just a signature. Any type that implements the trait automatically gets that behavior "for free" without writing a single line of code for it — but it can still override the method with its own implementation if the default doesn’t fit. This single feature is how Rust avoids class inheritance while still letting you share behavior across many types: you get the best of both worlds, a flat, non-hierarchical type system plus reusable logic.
Overview: What a Default Trait Method Is
Think of a trait as a contract: it lists the methods a type must support to "count" as that trait. Normally every method in that contract is just a signature — a promise with no body — and every implementing type has to supply its own code for it. A default method is different: the trait author writes an actual { ... } body directly inside the trait definition. When a type writes impl Trait for Type, the compiler checks each method in the trait: if the impl block provides that method, the compiler uses that version; if it doesn’t, and the trait supplied a default body, the compiler quietly wires up the default instead. Nothing is inherited from a parent class, because there is no class hierarchy at all — the default body simply lives on the trait itself, and the compiler copies its behavior in wherever an implementor stays silent.
This matters because it lets a trait author build a small set of required methods (the minimal, type-specific primitives) and then layer richer default methods on top of them, calling self.required_method() from inside the default body. Every concrete type only has to implement the small, essential part; the rest of the behavior comes along automatically. This is sometimes called the "template method" pattern, and in Rust it replaces most of what other languages use base-class inheritance for.
Default Methods vs. the Default Trait
There are two different things in Rust that share the word "default," and it’s worth separating them clearly before going further. A default trait method (this lesson’s topic) is any method inside a trait definition that has a body, so implementors can skip writing it. The standard library’s Default trait is a completely separate, specific trait (std::default::Default) with one method, default(), that produces a "zero-ish" starting value for a type — and it’s usually generated automatically with #[derive(Default)] rather than written by hand. The two concepts are related only by name, not by mechanism: any trait can have default methods (that’s a language feature), while Default is just one ordinary trait in the standard library (that happens to have exactly one method and no default body of its own).
#[derive(Debug, Default)]
struct Config {
verbose: bool,
retries: u32,
name: String,
}
fn main() {
let cfg = Config::default();
println!("{:?}", cfg);
}
Output:
Config { verbose: false, retries: 0, name: "" }
Here #[derive(Default)] generates an implementation of the Default trait for Config, which fills every field with its own default value (false for bool, 0 for u32, an empty String for String). That’s unrelated to a trait method having a default body — it’s just an unfortunate naming coincidence. The rest of this lesson is entirely about the first concept: methods inside a trait definition that carry their own implementation.
Syntax
The general shape is a trait with a mix of required and default methods:
trait TraitName {
// Required: no body -- every implementor must provide this.
fn required_method(&self) -> String;
// Default: has a body, so implementors get it for free.
fn optional_method(&self) -> String {
format!("default result based on {}", self.required_method())
}
}
| Aspect | Required method | Default method |
|---|---|---|
| Body in the trait definition | None — the signature ends with a semicolon | Has a full { ... } body |
| Must every implementor supply one? | Yes — omitting it is a compile error | No — it’s inherited automatically unless overridden |
| Can it be overridden? | N/A, it must be written anyway | Yes, by defining a method of the same name/signature in the impl block |
| Typical role | The minimal, type-specific primitive | Shared, richer behavior built from the primitives |
A trait is free to mix any number of required and default methods, and a default method may call any other method declared in the same trait — required or default — through self.
Examples
Example 1: A Default Greeting, Overridden by One Type
trait Greet {
fn name(&self) -> String;
fn greet(&self) -> String {
format!("Hello, {}!", self.name())
}
}
struct Person {
name: String,
}
impl Greet for Person {
fn name(&self) -> String {
self.name.clone()
}
}
struct Robot;
impl Greet for Robot {
fn name(&self) -> String {
String::from("Robot")
}
fn greet(&self) -> String {
format!("BEEP BOOP {} ONLINE", self.name().to_uppercase())
}
}
fn main() {
let alice = Person { name: String::from("Alice") };
let r2d2 = Robot;
println!("{}", alice.greet());
println!("{}", r2d2.greet());
}
Output:
Hello, Alice!
BEEP BOOP ROBOT ONLINE
Greet requires only name; greet has a default body. Person implements just name and inherits the default greet unchanged, so calling alice.greet() runs the trait’s own code. Robot implements name too, but also writes its own greet, so the compiler uses Robot‘s version instead of the trait’s default — the default is simply never consulted for that type.
Example 2: A Default Method That Calls Another Default Method
trait Shape {
fn area(&self) -> f64;
fn describe(&self) -> String {
format!("This shape has an area of {:.2}", self.area())
}
fn is_larger_than(&self, other: &dyn Shape) -> bool {
self.area() > other.area()
}
}
struct Circle {
radius: f64,
}
impl Shape for Circle {
fn area(&self) -> f64 {
std::f64::consts::PI * self.radius * self.radius
}
}
struct Square {
side: f64,
}
impl Shape for Square {
fn area(&self) -> f64 {
self.side * self.side
}
}
fn main() {
let circle = Circle { radius: 2.0 };
let square = Square { side: 3.0 };
println!("{}", circle.describe());
println!("{}", square.describe());
println!("Is the circle larger than the square? {}", circle.is_larger_than(&square));
}
Output:
This shape has an area of 12.57
This shape has an area of 9.00
Is the circle larger than the square? true
Shape requires only area. Both describe and is_larger_than are default methods that call self.area() — neither Circle nor Square overrides them, so both types share exactly the same describe/compare logic while computing area completely differently. This is the template-method pattern in action: one required primitive, several free behaviors built on it.
Example 3: Overriding a Default in a Generic Function
trait Summary {
fn title(&self) -> String;
fn summarize(&self) -> String {
String::from("(Read more...)")
}
}
struct Article {
title: String,
body: String,
}
impl Summary for Article {
fn title(&self) -> String {
self.title.clone()
}
fn summarize(&self) -> String {
let preview: String = self.body.chars().take(20).collect();
format!("{}: {}...", self.title(), preview)
}
}
struct Tweet {
handle: String,
}
impl Summary for Tweet {
fn title(&self) -> String {
format!("@{}", self.handle)
}
}
fn print_summary(item: &impl Summary) {
println!("{} -> {}", item.title(), item.summarize());
}
fn main() {
let article = Article {
title: String::from("Rust 2.0 Released"),
body: String::from("The Rust team announced a major update today."),
};
let tweet = Tweet {
handle: String::from("rustlang"),
};
print_summary(&article);
print_summary(&tweet);
}
Output:
Rust 2.0 Released -> Rust 2.0 Released: The Rust team announ...
@rustlang -> (Read more...)
print_summary takes &impl Summary, so it works with any type that implements the trait, without knowing whether that type overrides summarize. Article writes its own summarize that builds a real preview from its body text. Tweet only implements title, so it falls back to the trait’s placeholder summarize. The caller doesn’t need to know or care which one happened — that’s exactly the point of default methods.
How It Works Step by Step
When you write value.method() and method comes from a trait, the compiler resolves it in a fixed order:
- Look at the concrete type’s
impl Trait for Typeblock. If it definesmethoditself, that implementation is used — full stop. - Otherwise, check whether the trait definition supplied a default body for
method. If it did, that body is used, withselfbound to the concrete type. - If neither exists, the code doesn’t compile — a required method with no override and no default is a hard error.
This resolution happens purely at compile time; there’s no lookup happening while your program runs. For static dispatch (a generic function like fn f<T: Trait>(x: T), or &impl Trait as in Example 3), the compiler monomorphizes: it generates a separate copy of the function for each concrete type actually used, and each copy calls whichever method — override or default — that type resolved to. There is zero indirection at runtime; it’s exactly as if you’d hand-written a version of the function per type. For dynamic dispatch (a &dyn Trait trait object, as is_larger_than takes in Example 2), the compiler instead builds a vtable per concrete type — a small table of function pointers. Each entry points either at that type’s override or, if none exists, at the trait’s shared default implementation. Calling a method on a dyn Trait value is one indirect jump through that table. Either way, a default method costs nothing extra compared to a hand-written one; "default" only affects who has to write the code, not how it executes.
It’s also worth internalizing why a default method is even allowed to call self.required_method() before any concrete type is known: inside the trait definition, the compiler only needs to know that whatever Self ends up being, it will implement Trait (that’s what being inside the trait’s own impl scope guarantees). So self.required_method() type-checks against the trait’s signature, and gets resolved to the real, concrete implementation only once a specific type is plugged in at the call site.
Common Mistakes
Mistake 1: Assuming a Default Method Covers the Whole Trait
A default method doesn’t excuse you from implementing the trait’s required methods. Forgetting one is a compile error, even if every method you skipped happens to have a default:
trait Shape {
fn area(&self) -> f64;
fn describe(&self) -> String {
format!("Area: {:.2}", self.area())
}
}
struct Triangle {
base: f64,
height: f64,
}
// error[E0046]: not all trait items implemented, missing: `area`
impl Shape for Triangle {}
fn main() {
let t = Triangle { base: 3.0, height: 4.0 };
println!("{}", t.describe());
}
The compiler rejects this because area has no default body — it’s required, and describe‘s default body depends on it existing. The fix is simply to implement the required method:
trait Shape {
fn area(&self) -> f64;
fn describe(&self) -> String {
format!("Area: {:.2}", self.area())
}
}
struct Triangle {
base: f64,
height: f64,
}
impl Shape for Triangle {
fn area(&self) -> f64 {
0.5 * self.base * self.height
}
}
fn main() {
let t = Triangle { base: 3.0, height: 4.0 };
println!("{}", t.describe());
}
Output:
Area: 6.00
Mistake 2: Trying to Call "the Default" from Inside an Override
Programmers coming from languages with class inheritance often reach for something like super.method() to run the parent’s version and add to it. Rust has no super keyword for traits, and calling self.method() from inside an override of method does not reach the trait’s default — it calls the override itself, recursing forever:
trait Greet {
fn name(&self) -> String;
fn greet(&self) -> String {
format!("Hello, {}!", self.name())
}
}
struct Robot;
impl Greet for Robot {
fn name(&self) -> String {
String::from("Robot")
}
fn greet(&self) -> String {
// Mistake: there is no `super`. This calls Robot's own
// `greet` again -- not the trait's default -- and recurses forever.
format!("{} (custom)", self.greet())
}
}
This compiles fine (it’s a legal, if useless, function), which is exactly what makes it dangerous: the bug only shows up when the program runs, as unbounded recursion and a stack overflow. The fix is to factor the shared logic into its own method that both the default path and the override can call by name, instead of trying to call "the version before mine":
trait Greet {
fn name(&self) -> String;
fn base_greeting(&self) -> String {
format!("Hello, {}!", self.name())
}
fn greet(&self) -> String {
self.base_greeting()
}
}
struct Robot;
impl Greet for Robot {
fn name(&self) -> String {
String::from("Robot")
}
fn greet(&self) -> String {
format!("{} (custom)", self.base_greeting())
}
}
fn main() {
let r2d2 = Robot;
println!("{}", r2d2.greet());
}
Output:
Hello, Robot! (custom)
base_greeting is a separate default method that Robot never overrides, so both the trait’s own default path and Robot‘s override of greet can call it safely by name.
Best Practices
- Keep required methods to the smallest set of type-specific primitives, and build richer behavior as default methods on top of them — this is the template-method pattern and it minimizes boilerplate for every implementor.
- Don’t try to call "the original default" from inside an override; Rust has no
super. If a default and an override need to share logic, factor that logic into its own separate method (as in thebase_greetingfix above) or a free function. - Document what a default method assumes and when implementors should override it — defaults are easy to silently inherit without anyone noticing.
- If truly every implementor needs different behavior for a method, make it required, not default; a default that everyone overrides is just dead code with extra steps.
- Use
&dyn Traitwhen you need a collection of different concrete types behind one runtime interface; use a generic<T: Trait>or&impl Traitparameter when the concrete type is known at each call site and you want the compiler to monomorphize for maximum performance. - Remember that
Default(the standard-library trait, usually reached via#[derive(Default)]) is a different concept from a trait method having a default body — don’t let the shared word cause confusion in your own trait designs.
Practice Exercises
- Define a trait
Animalwith a requiredfn name(&self) -> Stringand a defaultfn speak(&self) -> Stringthat returnsformat!("{} makes a sound.", self.name()). Implement it for astruct Dogthat overridesspeakto return"Woof!", and astruct Catthat uses the default. Print both. Expected output includes one custom line and one that ends in "makes a sound." - Take the
Shapetrait from Example 2 and add a new default methodfn scale_description(&self, factor: f64) -> Stringthat reports what the area would be if the shape’s linear dimensions were multiplied byfactor(area scales byfactor * factor). Call it on aCirclewithfactorof2.0and check that the reported area is four times the original. - Write a trait
Loggerwith a defaultfn level(&self) -> &strreturning"INFO"and a defaultfn log(&self, message: &str)that prints[LEVEL] messageusingself.level(). Implement it for astruct DebugLoggerthat overrides onlylevelto return"DEBUG", and confirm thatlogautomatically picks up the overridden level without you touchinglogitself.
Summary
- A trait method with a
{ ... }body in the trait definition is a default method: implementors inherit it automatically and may override it by writing their own version with the same signature. - A method with no body (ending in
;) is required: every implementor must supply it, or the code fails to compile. - Default methods may call any other trait method, including required ones, through
self— this is the template-method pattern and it’s the main reason to reach for default methods at all. - Method resolution is decided per type at compile time: an override always wins over the default; static dispatch monomorphizes per type, dynamic dispatch routes through a vtable entry, and either way a default method costs nothing extra at runtime.
- The standard library’s
Defaulttrait (usually via#[derive(Default)]) is an unrelated concept that just happens to share the word "default." - There is no
superin Rust — if a default and an override need to share code, factor that code into its own separate method instead of trying to call "the version before mine."
