-
Notifications
You must be signed in to change notification settings - Fork 13.6k
Description
I tried this code:
use std::borrow::Cow;
use std::path::Path;
fn func(_: impl AsRef<Path>) {}
fn main() {
let s: &str = ".";
let path: &Path = Path::new(&s);
func(path);
func(&path);
func(Cow::Borrowed(path));
func(s);
func(&s);
func(Cow::Borrowed(s));
}
I expected to see this happen: Code compiles and runs without error.
Instead, this happened:
error[E0277]: the trait bound `Cow<'_, str>: AsRef<Path>` is not satisfied
--> src/main.rs:14:10
|
14 | func(Cow::Borrowed(s));
| ---- ^^^^^^^^^^^^^^^^ the trait `AsRef<Path>` is not implemented for `Cow<'_, str>`
| |
| required by a bound introduced by this call
|
= help: the following other types implement trait `AsRef<T>`:
<Cow<'_, OsStr> as AsRef<Path>>
<Cow<'_, T> as AsRef<T>>
note: required by a bound in `func`
--> src/main.rs:4:17
|
4 | fn func(_: impl AsRef<Path>) {}
| ^^^^^^^^^^^ required by this bound in `func`
For more information about this error, try `rustc --explain E0277`.
More examples of (allegedly) inconsistent behavior can be found in this comment below. The cause is subtle and I suspect it is in std
. I believe that
impl<T: ?Sized + ToOwned> AsRef<T> for Cow<'_, T> {
fn as_ref(&self) -> &T {
self
}
}
should actually be:
impl<T, U> AsRef<U> for Cow<'_, T>
where
T: ?Sized + ToOwned + AsRef<U>,
U: ?Sized,
{
fn as_ref(&self) -> &U {
self.deref().as_ref()
}
}
That is because .as_ref()
can't generally be used to go from T
to &T
. It doesn't need to go from Cow<'_, T>
to &T
either. We have .borrow()
for that, because there is an impl<T: ?Sized> Borrow<T> for T
in std
.
Instead, Cow
should be transparent in regard to AsRef
, i.e. if a type T
implements AsRef<U>
, then Cow<'_, T>
should implement AsRef<U>
too. Compare &T
, which implements AsRef<U>
when T: AsRef<U>
(which is the reason why we can't have a generic impl<T: ?Sized> AsRef<T> for T
).
See also this post on URLO.
Fixing this may be a breaking change and/or require a new edition, I guess.
Update moved up from comment below:
Note that Rc
and Arc
behave consistent to Cow
:
impl<T: ?Sized> AsRef<T> for Rc<T>
impl<T: ?Sized> AsRef<T> for Arc<T>
impl<T: ?Sized + ToOwned> AsRef<T> for Cow<'_, T>
All three are inconsistent with &
:
I think that generic(!) smart pointers such as Cow
, Rc
, and Arc
should behave the same as ordinary shared references in regard to AsRef
, but they do not. Maybe there is a good reason why this isn't the case. In either case, it's not possible to solve this without breaking a lot of existing code, so I propose:
- Keeping this issue in mind for future overhaul of std (if ever possible).
- Documenting this issue properly.
Meta
rustc --version --verbose
:
rustc 1.64.0-nightly (495b21669 2022-07-03)
binary: rustc
commit-hash: 495b216696ccbc27c73d6bdc486bf4621d610f4b
commit-date: 2022-07-03
host: x86_64-unknown-freebsd
release: 1.64.0-nightly
LLVM version: 14.0.6
Activity
JanBeh commentedon Jul 4, 2022
Also compare @conradludgate's
Owned
implementation, which contains a correctAsRef
implementation (apart from missingU: ?Sized
) and the cratederef_owned
version 0.8.2, which also has a correctAsRef
implementation for theOwned
wrapper (which is similar to the enum variantCow::Owned
).Rageking8 commentedon Jul 5, 2022
@rustbot label T-compiler
QuineDot commentedon Jul 5, 2022
Arguably for your example, all that's missing is PR #73390. See that PR to see how it breaks inference in existing code, which your suggestion would do as well.
Definitely breaking without PR #39397 (which would probably be more disruptive than PR #73390), as
Cow<'_, T>
will no longer implementAsRef<T>
whenT
doesn't implementAsRef<T>
.JanBeh commentedon Jul 5, 2022
PR #73390 would be a workaround instead of solving the problem by fixing its cause. Note that it's not possible for external code to provide this workaround for types that are not defined in
std
.See this extended example:
I would also expect this code to compile and run without error, but it doesn't:
It's also (obviously) not possible to add a workaround where needed:
Which gives (this is not unexpected):
I conclude that PR #73390 is semantically wrong. Instead the cause of the problem should be fixed as proposed in my original post.
I understand that this is breaking. However, given the current semantics of
AsRef
andBorrow
, I considerimpl<'a, T: ?Sized> AsRef<T> for Cow<'a, T>
to be an error. I also consider the following example code to be semantically wrong (i.e. I do not think that the following example must compile (edit: unless we haveimpl<T: ?Sized> AsRef<T> for T
one day), even though itmay anddoes):If the cause of the issue cannot be fixed, it should be at least documented as an error in
Cow
's documentation.However, leaving
Cow
as it is is pretty ugly, considering thatCow
is a pretty basic data type and should act like a reference replacement.JanBeh commentedon Jul 5, 2022
For a meta discussion on this kind of problems, I opened a new thread on IRLO: Rust's complexity + Rust's stability policy = ?
QuineDot commentedon Jul 5, 2022
That's why I used the phrasing I did. The point is, even fixing the single use-case of the original example was deemed too disruptive, and it isn't even a major breaking change.
Even if there's a way to keep the inference breakage from being too disruptive, it's a major breaking change that can't happen before intersection specialization or Rust 2.0, modulo some other reversion of Rust's backwards compatibility guarantees. That is, the working example you give should indeed and is indeed guaranteed to work, must compile, as per the blanket implementation on
Cow
which has existed since (before) Rust 1.0.0.I agree the existing medley of traits and implementations are not perfect, but sometimes theoretical purity must bow to practicality.
JanBeh commentedon Jul 5, 2022
A potential migration path might be to provide something like
deref_owned::GenericCow
, deprecateCow
, and reintroduce a fixed version ofCow
, where both the old and the new version ofCow
implementGenericCow
.JanBeh commentedon Jul 5, 2022
I had an error in my original proposed solution. This has been corrected (by editing) as follows:
JanBeh commentedon Jul 6, 2022
Instead of seeing the cause of the problem in
Cow
sAsRef
implementation (as assumed in my original post), the real cause might also be seen much deeper here:See @QuineDot's post on URLO. Following his reasoning, many occurrences of
P where P: impl AsRef<Path>
should rather be&P where P: impl ?Sized + AsRef<Path>
.All this is non-fixable, of course.
However, for the sake of completeness, I would like to outline another (hypothetical) non-breaking solution for this issue in the following.
I previously proposed:
But instead,
AsRef
could be deprecated and be replaced with a new version (calling itAsRef2
for purpose of referencing) that doesn't provide animpl<T: ?Sized, U: ?Sized> AsRef2<U> for &T
but insteadimpl<T: ?Sized> AsRef2<T> for T
.AsRef2
would need to be used differently, for example:Then
Cow
might or might not implementAsRef2
in the same way asCow
currently implementsAsRef
, or not implementAsRef2
at all (this is a detail question, I believe).Please note that this is not a suggested solution but merely a thought-experiment to better understand the cause of the problem.
JanBeh commentedon Jul 7, 2022
I would like to state that the thesis / topic of this issue report
might require far more discussion, see second (technical) part of my post here.
I.e. I'm not sure if my proposed (hypothetical) solutions really are the right way to go (apart from being practical). The underlying problems might be more complicated/tricky.
I would like to clarify that I do not propose any immediate action on this issue but wanted to point out inconsistencies, of which some can be seen here:
(Playground)
And here:
(Playground)
JanBeh commentedon Jul 7, 2022
There would be two potential ways to fix this, of which none are really feasible at this point, I believe:
Migration path A:
Note that this would also need to be done with
Rc
,Arc
, and maybe more types (which would make migration even more painful).Migration path B:
Choosing migration path B, one of the following mutually exclusive implementations could be chosen:
impl<T: ?Sized> AsRef2<U> for &T
(variant B1)impl<T: ?Sized + AsRef2<U>, U: ?Sized> for &T
(variant B2)It is possible that variant B1 would cause practical implications due to lack of transitivity of
AsRef
/AsRef2
(which is likely why variant B2 was originally chosen forAsRef
in Rust).In no case either of these migration paths appear feasible by now, but the inconsistency remains.
Edit: Moved some parts to the OP for a better overview.
8 remaining items