impl Trait in Rust: Argument, Return & Async Fn Use

Max WellsMax WellsFounder of Rustify

TL;DR: impl Trait appears in two positions: argument position (fn foo(x: impl Display)) and return position (fn foo() -> impl Display). In argument position it's syntactic sugar for a generic. In return position it lets you return a concrete type without naming it, the compiler knows the exact type, so there's zero overhead. This is static dispatch. Contrast with dyn Trait, which erases the type and dispatches at runtime via vtable, slightly slower but allows heterogeneous collections.


What Is impl Trait in Argument Position?

impl Trait in argument position is shorthand for a generic type parameter, each call gets monomorphized to the concrete type.

use std::fmt::Display;
 
// These three are equivalent:
fn print_a(x: impl Display) { println!("{x}"); }
fn print_b<T: Display>(x: T) { println!("{x}"); }
fn print_c<T>(x: T) where T: Display { println!("{x}"); }
 
fn main() {
    print_a(42);          // monomorphized to print_a::<i32>
    print_a("hello");     // monomorphized to print_a::<&str>
}

impl Trait is cleaner for simple single-bound cases. Use the explicit <T: Bound> form when you need to refer to T multiple times or use where clauses.


What Is impl Trait in Return Position?

-> impl Trait lets a function return a concrete type without naming it, useful for closures, iterators, and async functions where the return type is complex or unnameable.

// The closure type has no name; impl Trait lets us return it
fn make_adder(x: i32) -> impl Fn(i32) -> i32 {
    move |y| x + y
}
 
// The iterator chain type is incredibly complex; impl Iterator hides it
fn evens_up_to(n: u32) -> impl Iterator<Item = u32> {
    (0..n).filter(|x| x % 2 == 0)
}
 
fn main() {
    let add5 = make_adder(5);
    println!("{}", add5(3)); // 8
 
    for n in evens_up_to(10) {
        print!("{n} "); // 0 2 4 6 8
    }
}

The compiler knows the exact concrete type, it generates specialized code for it. No heap allocation, no vtable.


How Is impl Trait Different From dyn Trait?

impl Trait = static dispatch (monomorphization, zero overhead, single concrete type). dyn Trait = dynamic dispatch (vtable, small overhead, allows multiple concrete types).

use std::fmt::Display;
 
// impl Trait; caller and function agree on ONE type at compile time
fn static_print(x: impl Display) {
    println!("{x}");
}
 
// dyn Trait; type resolved at runtime via vtable
fn dynamic_print(x: &dyn Display) {
    println!("{x}");
}
 
// CRITICAL difference; impl Trait cannot do this:
fn make_shape(circle: bool) -> Box<dyn Display> {
    if circle {
        Box::new("circle")   // &str
    } else {
        Box::new(42i32)       // i32
    }
    // impl Trait cannot return two different types
}
impl Traitdyn Trait
DispatchStatic (compile-time)Dynamic (runtime vtable)
PerformanceZero overheadSmall vtable indirection
Multiple types❌ one type per call site✅ heterogeneous collections
Heap allocationNot requiredUsually Box<dyn Trait>
async fn returns✅ naturalNeeds boxing (async-trait crate)

When Should You Use Each?

Default to impl Trait. Switch to dyn Trait when you need a collection of mixed types, or when the concrete type must be unknown at compile time.

// Use impl Trait: single type, no heap, zero cost
fn process(iter: impl Iterator<Item = i32>) -> i32 {
    iter.sum()
}
 
// Use dyn Trait: multiple concrete types in same Vec
fn handlers() -> Vec<Box<dyn Fn(i32) -> i32>> {
    vec![
        Box::new(|x| x + 1),
        Box::new(|x| x * 2),
        Box::new(|x| x - 3),
    ]
}

How Does impl Trait Work With async fn?

async fn implicitly returns impl Future<Output = T>. This is impl Trait in return position, the compiler names the state machine type internally.

// These are equivalent:
async fn fetch() -> String { "hello".to_string() }
 
fn fetch_explicit() -> impl std::future::Future<Output = String> {
    async { "hello".to_string() }
}

For dyn futures (trait objects), you need boxing: Box<dyn Future<Output = T> + Send>. This is what the async-trait crate does under the hood.


Frequently Asked Questions

No, impl Trait in return position must return exactly one concrete type. If a function might return one of two types, use dyn Trait wrapped in Box, or return an enum with variants for each type.

Not directly, impl Trait is only valid in function signatures. For struct fields with an unnamed type, use a generic parameter: struct Wrapper<T: Display> { inner: T }.

RPIT is the technical name for -> impl Trait. Rust 1.75 stabilized RPIT in trait definitions (async fn in traits), allowing async fn in traits without the async-trait crate for most cases.

In argument position, yes, they're equivalent. In return position, no, a generic parameter is chosen by the caller, while impl Trait in return position is chosen by the function. You can't call fn foo() -> impl Display and say "I want a String", the function decides.


Sources


  • Trait: impl Trait and dyn Trait are two ways to use traits in function signatures
  • Generic: impl Trait in argument position is sugar for a generic bound
  • Future: async fn returns impl Future, impl Trait in return position
  • Closure: impl Fn(...) is the common way to return closures from functions

Keep Reading

Ready to Land a $80-120k Rust Job?