Smart pointer which owns its target

I mostly agree, or rather,

// The addition of `?Sized` is the important part
fn bar<P: ?Sized + AsRef<Path>>(_: &P) {}

More on that below.

The practical fix is to dereference.


Written before at least some of your comment edits, and mostly just a big long exploration for the sake of a thought experiment -- nothing useful pertaining to your problems or practical for the future of Rust. Feel free to ignore everything below.

As a mental exercise, it is interesting to imagine how things could have been implemented differently. AsRef has its blanket implementation for APIs like File::open, and AsRef<Path> in particular was a significant motivator. Could we have done something different and gotten AsRef<T> for T instead of the &-nesting it overlaps with? [1]

An aside about `Into`:

One interesting note in the RFC is the section Why the reference restrictions?. There we can see that the author explicitly wanted to avoid HRTBs [2] such as:

fn bar<P: ?Sized>(_: &P) where for<'any> &'any P: Into<&'any Path>

And later they say that AsRef implies Into. However, the blanket implementation making AsRef imply Into was later removed as it conflicts with Into being reflexive.

This theme of wanting to avoid complex signatures comes up again below. That said, I feel it's a worthy separation of concerns in this case anyway; I'd hate to have to write (&x).into() and hope I got it right.


Now, if we look at File::open and friends, we can see that they use the pattern I asserted was an anti-pattern:

pub fn open<P: AsRef<Path>>(path: P) -> Result<File>

Which I guess I should back up:

  • Open can't do anything but create the &Path so it has no need for ownership (of a PathBuf, say)
  • as_ref takes a reference as well, so you're not really losing much by requiring a reference as an input, just...
  • ...the ability to pass something owned into open [which] is something you usually don't want to do
    • Because then you, the caller, can't use it anymore [3]
    • A hint to use AsRef would help, but there is none in this case, so the unwary will probably .clone unnecessarily upon getting a "used after move" error
    • (Or a hint to just use & for that matter, given the &-nesting support)

And one more pertinent quality:

  • If you use this pattern, you need to support some level of &-nesting in the implementations, because you can't have P = Path (or any other unsized type) -- you need AsRef<Path> for &Path so you can pass in the P = &Path.

We can't change those patterns now, because those functions accept owned types directly. Bummer. If we had instead

pub fn open<P: ?Sized + AsRef<Path>>(path: &P) -> Result<File>

We, arguably at least, would not have needed the nested & implementation on AsRef. [4] As it turns out though, that impl was included from the start. And hey, if you read the RFCs closely, they actually did use this ?Sized and &P pattern in the Path reform discussion (though often forgetting to write ?Sized). So what gives?

PR 23316:

it's much more ergonomic to not deal with ?Sized at all and simply require an argument P instead of &P.

This change is aimed at removing unsightly ?Sized bounds while retaining the same level of usability as before.

So because it's unsightly :roll_eyes:, we lost the more correct (IMO) implementations forever :-1:. [5]

That said, again, the PR is not what prompted the &-nesting implementation, so further insight would have been needed to avoid it. And maybe it justifies its existence in other ways I haven't thought of. [6]

Well then, should we add more conversion traits and deprecate all the functions with the anti-pattern? No, the current situation works well enough in practice, flawed though it may be. A change like that would be massively disruptive to the ecosystem.


One more anti-pattern-esque thing I'll note which applies to both of

pub fn open<P: ?Sized + AsRef<Path>>(path: &P) -> Result<File>
pub fn open<P: AsRef<Path>>(path: P) -> Result<File>

They both potentially save you some typing to call over taking a &Path (no .as_ref()), but as a result you get a monomorphized version of the entire function for every type used. You can mitigate this by doing a one-line as_ref() call and then passing off to a non-generic private method that takes &Path, but I suspect most people copying std conventions don't do so. [7]

In summary, optimizing for writing is a bane upon programming. :wink: [8]


OK -- congrats on making it this far by the way -- I contemplated the above exploration in the context of your AsRef<T> as T thread after seeing your filed issue, because it reminded me about how the P: AsRef<Path> pattern is common but sub-optimal. In particular, I wasn't really taking the context of this thread into account; it ended up here by accident. [9]

But as a post-script, would having AsRef<T> for T solved the supposed problem in this thread? Well, we can't have both of

impl<X: ?Sized> AsRef<X> for X { /* ... */ }
impl<U: ?Sized, T: ?Sized + ToBorrow + AsRef<U>> AsRef<U> for Cow<'_, T> { /* ... */ }

because T might implement AsRef<Cow<'static, T>>, in which case they overlap. So without specialization, I don't think it would have really helped you out. Also, apparently, you found some arrangements of traits you do like which relies on the &-nesting (in a recent comment edit), so that's sort of amusing. It still must lose out on AsRef<T> for Cow<'_, T>.

Question though -- and again, sorry if this is just me not reading the thread thoroughly -- is there a practical reason you care, or is this all an "ideal design" exercise? I see a lot of types and calls in the example that just don't make a lot of sense to care highly about to me (though they can come up due to metaprogramming sort of easily I suppose). Having a Cow<'_, PathBuf> is like taking a &PathBuf instead of a &Path, or a &String instead of a &str, for example.

And particularly, in the context of the anti-pattern, in what situation do you own a Cow that you're okay throwing away in a call to something that wants AsRef<Path>? It can happen, but I think it's pretty rare, for the same reasons accepting PathBuf is a stumbling block -- if you have a (potentially) owned version, you're probably going to need it later. And you seemed okay entertaining the idea of avoiding the anti-pattern.

So if we discard that use-case [10], what are the remaining situations -- those where you don't want to give away the Cow? I think the top few would be

  • You own the cow and you're going to have to pass &cow or cow.as_ref() anyway so as to not give it away
    • So no great gain over having to do &*cow or the like
  • Your Cow is a field and you can't just pass self.cow either
    • So similar to the previous situation
  • You own cow_ref: &Cow<'_, T>
    • but that should probably be a &T instead
  • You own cow_mut: &mut Cow<'_, T>
    • but that should probably be a &mut <T as ToOwned>::Owned (or &T) instead

I think this isn't a bigger deal in the ecosystem because it just doesn't come up much, and when it does it's easy to work around. The most frequent requests do seem to be of the AsRef<Path> for Cow<'_, str> variety specifically. Maybe you're hitting it because you're trying to be super generic?


  1. I use "&-nesting" as a synonym for "As lifts over &". ↩︎

  2. and where clauses more generally ↩︎

  3. unless it's Copy ↩︎

  4. And the "oops I gave away ownership" speed bump would be removed, though you might need to explicitly deref where you don't today. ↩︎

  5. Ergonomics of the std writer shouldn't outweigh good design for the std consumer. ↩︎

  6. So I guess the takes-ownership pothole is the main fallout from that PR, combined with the fact that others (including myself) often copy std's patterns, so the anti-pattern spreads. ↩︎

  7. Related RFE. ↩︎

  8. Less cheekily, using generics for ergonomics is hard to get right, and also hard to change without breaking something. ↩︎

  9. And I've only skimmed this one, so pardon me if I retread old ground. ↩︎

  10. i.e. let's say you don't use the anti-pattern so you can't take owned values ↩︎

1 Like