Function arguments of different implentations of same trait

I'm writing a function that accepts two AsRef<Path> arguments, but they may have different concrete types. For example, one could be a tempfile::Tempdir and the other a String. Is it more idiomatic to write it as

fn foo<P1, P2>(p1: P1, p2: P2)
    P1: AsRef<Path>,
    P2: AsRef<Path>,

or as

fn foo(p1: &AsRef<Path>, p2: &AsRef<Path>)

The first one is more idiomatic. The second one makes use of trait objects, which add an extra layer of indirection (a vtable), so are generally avoided unless you want that indirection for some reason. On the other hand, the first one uses monomorphization, which specializes the function for each type it's called with in order to remove the need for indirection. In this case, you definitely don't need the indirection, since you're going to ignore the type right after calling as_ref anyway. The ergonomics are about the same in either case.

If you're using Rust 1.26+, you can also write it using the impl Trait syntax, which has the same benefits outlined by @quadrupleslap:

use std::path::Path;

fn foo(p1: impl AsRef<Path>, p2: impl AsRef<Path>) {}

Whether the generic or the impl AsRef version is more idiomatic I don't think we know yet, though!

(Here's a Rust Playground with that code.)


The trait object version will require the caller to take (or have) a reference, which prevents them from passing you an owned object if they don’t care about it anymore. The generics version would allow that, so there’s an ergonomics and usability improvement there.

However, you’ll be looking at potentially longer compile times and code bloat if you use lots of different combinations of the parameters. It’s unlikely that getting the &Path is actually critical for performance so any inlining that occurs won’t buy you much.

If you’re considering the trait object reference approach, you may as well consider just taking two &Path references directly as well. It makes for slightly less ergonomics on the caller and they can’t hand you an owned object either. But, that’s not really that terrible here.

If you really want max flexibility for the caller, the generic version is best.

1 Like

I think this is exactly the sort of situation where impl Trait in argument position comes is handy :slight_smile: