It's mostly me being pedantic about a pet peeve, but let's walk through it. (It's a lot to read about something that doesn't much matter, so also feel free to skip it.)
(You may know this part.) Type parameters like P
have an implicit Sized
bound, which makes sense when you're taking them by value -- you can't move things which aren't Sized
. str
isn't Sized
for example, which is why you're always using &str
. Path
is like str
. ?Sized
just means "remove the implicit Sized
bound".
References like &Path
are always Sized
. If your generic is always behind a reference (or other pointer, like in a Box
or Arc
), it can make sense to relax the bound and accept more types.
When we look at the idiomatic version:
fn gflw<P: AsRef<Path>>(by_value: P)
It has to be Sized
since we're taking by value. It's more ergonomic to call this with a PathBuf
or String
if the caller is okay giving up ownership, but IMO that's not terribly common. If they get an error that they moved their String
, they can add a reference or clone the value. The help suggests cloning, but taking a reference is the better solution. So this is a minor hazard.
The goal is just to get your hands on a &Path
. What happens if the caller passes in a &Path
? &Path
implements AsRef<Path>
because Path
implements AsRef<Path>
. So it goes something like this:
let path = by_value.as_ref();
// <&Path as AsRef<Path>>::as_ref takes a &&Path and calls (*self).as_ref:
// <Path as AsRef<Path>>::as_ref takes a &Path and returns self
Which is more indirect than necessary.
Those two points annoy me. Do they ultimately matter? No, not really, or very rarely. The indirection is probably always optimized out on --release
and would be cheap if not anyway. (But see below for a note about AsRef
more generally.) The cloning probably trips up newcomers mostly... or people who just let machines like their IDE apply the hint without thinking about it. Even then it's probably not horribly costly.
Now let's look at taking by reference, but without ?Sized
.
fn gflw<P: AsRef<Path>>(by_ref: &P)
You can't passed owned things anymore, but you can't pass a &Path
(or &str
) anymore either -- because then P = Path
and Path
is not Sized
. So you'd have to pass a &&Path
(or &&str
) which would not be ergonomic.
// v
gflw(&"foo")
In the body when passing &&Path
(so P = &Path
) you'd have
let path = by_ref.as_ref();
// <&Path as AsRef<Path>>::as_ref takes a &&Path and calls (*self).as_ref:
// <Path as AsRef<Path>>::as_ref takes a &Path and returns self
same as above.
Now we add ?Sized
.
fn gflw<P: ?Sized + AsRef<Path>>(by_ref: &P)
We can pass &Path
again, regaining the ergonomics for that case. And when passing &Path
, we have P = Path
, and the body goes like so:
let path = by_ref.as_ref();
// <Path as AsRef<Path>>::as_ref takes a &Path and returns self
One less layer of indirection.
Finally, a more general note about this AsRef<_>
pattern. Even when you just need a &Path
(or &str
etc), it's idiomatic (in the Sized
form) and also makes some things more ergonomic at the call site (passing in a &str
instead of calling .as_ref()
, say). But it also means you get multiple copies of your function, one for each type that's passed in ("monomorphization"). Ideally they would all compile down like so:
// which style we use is irrelevant to this section
fn gflw<P: AsRef<Path>>(path: P) {
fn the_real_gflw(path: &Path) {
// the rest of the function which uses `path`
}
the_real_gflw(path.as_ref())
}
However, in practice, you can't rely on this optimization happening. So in std
for example, they manually write the inner function out.
Then the chances of the one-liner generic function being inlined is high, and it ideally acts like the caller just called as_ref()
themselves.
Alternatively, if you're the only caller or you otherwise don't care about maximizing ergonomics at the call site, you can just take &Path
.