Semantics of AsRef

I was missing that part, even though I cited it in my OP. So the documentation does state that AsRef isn't reflexive (not in the overview in std::convert though, and it does so in the context of Borrow only).

Maybe it could be said more explicitly? Something like:

(example of how it could be phrased)

Trait std::convert::AsRef

Reflexivity

Ideally, AsRef would be reflexive, that is there is an impl<T: ?Sized> AsRef<T> for T, with as_ref simply returning &self. Such a blanket implementation is currently not provided due to technical restrictions of Rust's type system (it would be overlapping with another existing blanket implementation for &T where T: AsRef<U> which allows AsRef to auto-dereference, see "Generic Implementations" above).

A trivial implementation of AsRef<T> for T must be added explicitly for a particular type T where needed or desired. Note, however, that not all types from std contain such an implementation, and those cannot be added by crates due to orphan rules.

Therefore, writing .as_ref() cannot be used generally to go from a type to its reference, e.g. the following code does not compile:

let i: i32 = 0;
let r: &i32 = i.as_ref(); // let r: &i32 = &i; must be used instead

Maybe that's a bit easier to understand for newcomers. Particularly, it would explain that AsRef isn't lacking reflexivity for semantic but only for technical reasons (P.S.: and for backwards-compatibility, as I try to show further below).

Explaining this might help to reduce confusion on this subject for non-newcomers as well.

There are several methods in std which take a P: AsRef<Path> (which might or might not be considered to be over-generic). I would say it makes it easy to invoke them, but not sure if that has been a good design decision.

Getting back to some of the breaking examples in my OP, it should be said that circumventing these problems in practice isn't so difficult. We can do:

use std::borrow::Cow;
use std::ffi::OsStr;
use std::fs::File;

fn main() {
    let _: Result<File, _> = File::open("nonexistent");
    let _: Result<File, _> = File::open(&Cow::Borrowed(OsStr::new("nonexistent")));
    // This fails to compile:
    // let _: Result<File, _> = File::open(&Cow::Borrowed("nonexistent"));
    // If we have a reference to a `Cow`, we must use `&**`:
    let _: Result<File, _> = File::open(&**(&Cow::Borrowed("nonexistent")));
}

(Playground)

Just like you said, using &** (or &* if the Cow is owned and may be consumed). Not nice, but doable.

But these practical issues aside, I wonder if the use of .as_ref() in the following is (or rather: should be) considered idiomatic:

use std::borrow::Cow;
use std::fs::File;

fn main() {
    let cow: Cow<_> = Cow::Borrowed("nonexistent");
    let ref_to_cow: &Cow<_> = &cow;
    let _: Result<File, _> = File::open(&**ref_to_cow);
    let _: Result<File, _> = File::open(ref_to_cow.as_ref());
}

(Playground)

This works because of:

#[stable(feature = "rust1", since = "1.0.0")]
impl<T: ?Sized + ToOwned> AsRef<T> for Cow<'_, T> {
    fn as_ref(&self) -> &T {
        self
    }
}

(source)

I would like to state the hypothesis that:

  • while this implementation exists (and cannot be removed without breaking code), it should not have been added in the first place,
  • using ref_to_cow.as_ref() in the above Playground should be avoided in idiomatic code (hypothesis 4).

Moreover, I believe that existing code which uses .as_ref() in that way makes it impossible to solve the outlined problems by solely adding two blanket implementations (AsRef<T> for T and AsRef<U> for T where T::Target: AsRef<U>, e.g. with negative bounds) in the future.

This can be demonstrated by using my_as_ref as defined in my above post:

fn takes_path(_: impl MyAsRef<Path>) {}

fn main() {
    let cow: Cow<_> = Cow::Borrowed("nonexistent");
    let ref_to_cow: &Cow<_> = &cow;
    let _ = takes_path(&**ref_to_cow);
    let _ = takes_path(ref_to_cow.my_as_ref());
}

(Note that the first (reflexive) blanket implementation cannot provided for technical reasons in this demonstration but only a concrete implementation for str.)

(Playground)

Here type inference will fail due to multiple implementations of MyAsRef<_> for str:

   Compiling playground v0.0.1 (/playground)
error[E0282]: type annotations needed
  --> src/main.rs:78:35
   |
78 |     let _ = takes_path(ref_to_cow.my_as_ref());
   |                        -----------^^^^^^^^^--
   |                        |          |
   |                        |          cannot infer type for type parameter `T` declared on the trait `MyAsRef`
   |                        this method call resolves to `&T`

error[E0283]: type annotations needed
  --> src/main.rs:78:35
   |
78 |     let _ = takes_path(ref_to_cow.my_as_ref());
   |                        -----------^^^^^^^^^--
   |                        |          |
   |                        |          cannot infer type for type parameter `U`
   |                        this method call resolves to `&T`
   |
note: multiple `impl`s satisfying `str: MyAsRef<_>` found
  --> src/main.rs:51:1
   |
51 | impl MyAsRef<str> for str {
   | ^^^^^^^^^^^^^^^^^^^^^^^^^
...
66 | impl MyAsRef<Path> for str {
   | ^^^^^^^^^^^^^^^^^^^^^^^^^^
note: required because of the requirements on the impl of `MyAsRef<_>` for `Cow<'_, str>`
  --> src/main.rs:38:12
   |
38 | impl<T, U> MyAsRef<U> for T
   |            ^^^^^^^^^^     ^
help: use the fully qualified path for the potential candidates
   |
78 |     let _ = takes_path(<str as MyAsRef<Path>>::my_as_ref(&ref_to_cow));
   |                        +++++++++++++++++++++++++++++++++++          ~
78 |     let _ = takes_path(<str as MyAsRef<str>>::my_as_ref(&ref_to_cow));
   |                        ++++++++++++++++++++++++++++++++++          ~

Some errors have detailed explanations: E0282, E0283.
For more information about an error, try `rustc --explain E0282`.
error: could not compile `playground` due to 2 previous errors

Ironically just writing takes_path(ref_to_cow) would work! (Playground)

While these considerations are all very hypothetical and ignore that std doesn't do all these things, it might still be worth noting, because

  • being able to provide two blanket implementations for AsRef in the future would not automatically solve all the outlined problems but providing such a second blanket implementation for AsRef might instead induce more problems with existing code,
  • third party crates may go a different way than std.

Regarding the first point, I believe that the problem really cannot be solved without starting over with an entirely new AsRef (likely not possible at this point, but I still hope that somehow the Edition mechanism could be used in future to overcome bad design decisions of the past).

The latter point brings me back to the question:

Moreover, should .as_ref() be avoided in teaching material in cases where it's used to dereference (edit: generic(?)) smart-pointers?