Introduction
Rust offers two syntaxes for trait bounds in function arguments: impl Trait (argument-position impl Trait, or APIT) and explicit generic parameters. While they are equivalent in many cases, there are important differences that affect API design, especially around turbofish syntax and type inference.
Key Concepts
- APIT (Argument-Position impl Trait): Syntactic sugar that creates an anonymous generic parameter:
fn foo(x: impl Clone). - Explicit Generic: A named generic parameter:
fn foo<T: Clone>(x: T). - Turbofish: The
::<Type>syntax for specifying generic parameters explicitly. Not available with APIT.
Real World Context
Public library APIs must choose between APIT and explicit generics carefully. APIT is simpler for simple cases, but explicit generics are required when you need to name the type, use it multiple times, or let callers specify it with turbofish.
Deep Dive
Equivalence
For simple single-use parameters, they are equivalent:
rustfn print_it(item: impl Display) { println!("{item}"); } fn print_it<T: Display>(item: T) { println!("{item}"); }
When Explicit Generics Are Required
Multiple parameters of the same type:
rust// These are NOT equivalent: fn same_type<T: Clone>(a: T, b: T) { } // a and b must be the same type fn diff_type(a: impl Clone, b: impl Clone) { } // a and b can be different types same_type(1_i32, 2_i32); // OK // same_type(1_i32, "hi"); // Error: mismatched types diff_type(1_i32, "hi"); // OK: different anonymous types
Turbofish syntax:
rustfn create<T: Default>() -> T { T::default() } let x = create::<String>(); // Turbofish works with named generics // fn create_impl() -> impl Default { ... } // Can't turbofish this
Returning the same type as input:
rustfn identity<T>(x: T) -> T { x } // Return type matches input // fn identity(x: impl Clone) -> ??? { x } // Can't name the return type
When to Use APIT
Prefer APIT for simple, single-use bounds in private or internal functions:
rustfn log(msg: impl Display) { println!("[LOG] {msg}"); } fn process(iter: impl Iterator<Item = i32>) -> i32 { iter.sum() }
Common Pitfalls
- Using APIT when the type must be named — If you need the same type in multiple positions (arguments, return type, struct fields), you need explicit generics.
- Assuming APIT enables dynamic dispatch —
impl Traitin argument position is still static dispatch. For dynamic dispatch, use&dyn Trait.
Best Practices
- Use APIT for simple leaf functions — When the type appears once and is not returned, APIT is cleaner.
- Use explicit generics for public APIs — Named generics let callers use turbofish and make documentation clearer.
Summary
impl Traitin arguments creates an anonymous generic parameter.- Explicit generics are required for turbofish, repeated types, and return types.
fn foo(a: impl T, b: impl T)allows different types;fn foo<T>(a: T, b: T)requires the same type.- Prefer APIT for simple cases, explicit generics for public APIs.
Code Examples
use std::fmt::Display;
// APIT: simple, anonymous generic
fn log(msg: impl Display) {
println!("[LOG] {msg}");
}
// Explicit generic: needed when type appears multiple times
fn max_of<T: PartialOrd>(a: T, b: T) -> T {
if a > b { a } else { b }
}
// Both called the same way
log("server started");
log(42);
let m = max_of(10, 20); // T inferred as i32
let m = max_of::<f64>(1.5, 2.5); // Turbofish works with named generics