Method auto-(de)ref (and lack of it) in RFC 132 ufcs

I was a bit upset at RFC 132 for not having auto-(de)ref for recievers.

After some research I found:

  • Method auto-ref on the dot operator is present around the time of RFC 132 (evidence: code examples in RFC 241). I don't know why its lack in RFC 132 isn't noted at all in the RFC PR. Perhaps RFC processes were just not popular enough then? But I can't find anything in the 0.11.0 reference about method auto-ref on the dot operator, so maybe that feature is just altogether under-noted back then.

  • Method auto-deref semantics is added after RFC 132, so they naturally weren't considered then.

    RFC Started Merged
    #132 ufcs 2014-03-17 2014-08-06
    #241 deref-conversions 2014-09-16 2015-01-21

So I guess if #241 deref-conversions was there back then, and method auto-ref semantics were properly documented, then #132 ufcs would have considered it, e.g. add auto-(de)ref for it in T::method(it, ...) where fn method(&self, ...) [Self = T][1], allowing it to compile with it: Box<T>.

So it seems that ufcs lacking auto-(de)ref is just a historical problem, not something intended.

I guess ufcs (and method disambiguation in general[2]) is kinda rare such that people know it but haven't bothered enough to write an RFC for it. (There is RFC 3913, actually. (Pre-RFC)) And ufcs has been there for 10+ years such that its characteristic of lacking auto-(de)ref might have even been relied on. These are all my guesses because I don't use it often too. What do you think of these? Do you have experiences that prove or disprove these statements? And what do you think about adding auto-(de)ref to ufcs now?


  1. method may be on T or on a trait, should be the same anyway ↩︎

  2. including e.g. this pub trait Quz hack ↩︎

Hmm, RFC 132 explicitly says this:

  • It is sometimes desirable not to have autoderef:
  • For methods like clone() that apply to almost all types, it is convenient to be more specific about which precise type you want to clone. To get this right with autoderef, one must know the precise rules being used, which is contrary to the "DWIM" intention.
  • For types that implement Deref<T>, UFCS can be used to unambiguously differentiate between methods invoked on the smart pointer itself and methods invoked on its referent.

Because the self parameter loses its magic in UFCS (because that’s what UFCS means), it would be extremely surprising for the first argument to auto-ref/deref, which doesn’t happen with non-method associated functions that are called identically.

6 Likes

Whoops, missed that completely. :joy:

I'm just thinking... a flag to enable auto(de)ref in ufcs? Like T::method(autoref x, ...). The main point is that RFC 3913 I've linked is a postfix syntax that has auto(de)ref, and if it gets accepted, I don't want it to feel like "I have to switch from prefix (traditional ufcs) to postfix to get auto(de)ref, that just feels inconsistent".

Non-method calls do have deref coercion, fully qualified[1] or not.[2] But they don't have auto-ref.

What exactly are you wishing existed?

  • Shallow auto-ref or the full method resolution (auto-ref after every deref)?
  • All parameters or only the first or only receivers?
  • Depending on how qualified the path is or not?[3]
  • Applying when? Some choices would change program semantics.

  1. UFCS is a misnomer ↩︎

  2. I always considered the "desirable not to have autoderef" comment a bit off base. It's more, "desirable to be explicit about which trait impl you're calling". ↩︎

  3. including just a bare call ↩︎

1 Like

What I was thinking is:

  • The full method resolution
  • Only recievers
  • Probably going to require the method being qualified, or just require it's a method, but maybe not the latter, because if it's supported in unqualified paths (e.g. use Trait::method) it doesn't make sense in similar-looking functions (e.g. fn foo in my playground link)
  • Manually enable by putting autoref (on second thoughts, autoderef? not sure of the exact syntax) before the value

That is, <current ufcs syntax>::method(autoref x, ...) behaves like x.method(...) and the only difference is method selection.


Played with your example, you (and I, initially) seem to have made the mistake of forgetting to add & in Foo::foo(var), making it infer &Self = &mut StringSelf = String.

There was no mistake, it's a demonstration that introducing auto-ref to non-method coercions can result in calling different functions.

If it only applied to receivers with qualified paths that include the implementing type, so the target type is known... well... it's a lot harder to demonstrate that inserting auto-ref can change semantics, but not impossible. Probably no one relies on it? :sweat_smile:

1 Like

If T::method(x, ...) can mutate x (autoref to &mut T), that's a design issue in my opinion[1]. It's the case today that x as a value expression is always a move[2]. Technically x is a place expression that gets coerced to a value in argument position, but it would very confusing to make an existing value expression place treat place expressions differently. In method syntax x.method(...), x is treated as a place expression.

Thus the only version of autoref on argument position I could support would autoref not to &[mut] $expr semantics but instead to &[mut] { $expr } semantics, which puts the expression still in a value expression position, meaning that the source place gets moved from.

Additionally, in Rust as its designed today, methods aren't special; they're just standard functions that can be called just like any other associated function. It's method call syntax which has the special behavior. That's why I even support not just method disambiguation but full unified method call syntax, allowing any function value to be invoked with method call semantics, leaning into the method call syntax being what's special, not the method item. Autoref for UMCS receivers would make method items into special function items again in more than just availability to method name lookup.

However, I definitely have felt the desire for the ability to apply method-style autoref in macro expansions, where you typically want to call fully qualified names to ensure that the macro is not dependent on the namespace context that it's invoked in. This is probably a very niche use case, but I'd even appreciate the ability to do match k#autoref $x { x => { /* ... */ }} so that m!(x) only evaluates the expression argument a single time with the caller's lifetime extension context but can still apply autoref if applicable.[3]

I could, on the other hand, potentially support automatically reborrowing non-reference argument places. &mut argument places are special in that they're always passed as &[mut] *x, meaning that the binding x remains initialized after, even bypassing the mutability of the binding. This doesn't occur for other impl Deref; e.g. you can't do f(x) where x: Box<i32>, f: fn(&i32), you need to introduce the reference yourself as f(&x) in order for autoderef to project through. I remember the change that mostly eliminated the need to ever write &*. I didn't fully understand exactly how that works back then, but I think I now understand that it enabled autoderef to project into custom impl Deref if autoderef is already started (i.e. by a top-level primitive reference). I think nowadays we could justify removing this restriction, thus allowing automatic reborrows of non-reference types, at least non-mutably.

To make sure people understand my term usage, my mental model is, given place p:

  • autoref: &[mut] p
  • autoderef: &[mut] **p, potentially recursively
  • reborrow: &[mut] *p

In practice method name lookup is fundamentally intertwined with auto[de]ref, but extracted out of method syntax, autoref is just adding a correctly-mutable reference[4] that then can get autoderef'd to the correct pointee type.


  1. In the time up to and around 1.0, avoiding the concept of argument passing modes that C++ has was a much more prominent feature of Rust's design, so that potentially explains why RFC 132 doesn't feel the need to mention autoref. Autoref is what allows &self methods to be selected by method syntax, "obviously" it wouldn't apply to function call syntax. I'd even hazard a guess that the ability for method syntax to select <&T>::foo on a receiver of type T was unintentional; you couldn't impl for &T outside std until RFC 1023 was implemented. ↩︎

  2. Yes, even when T: Copy. The only difference is that moving from a Copy value does not invalidate the source place. Well... except for the special case for reborrowing &mut. ↩︎

  3. Yes, this is the exact semantics that I don't support when applying autoref to qualified method UFCS. ↩︎

  4. Obviously only if the target type is a reference. Notably, it does not require the self argument to be a reference; x.foo() can resolve to <&T>::foo(self). &_ has no inherent methods, so this has to be a trait method, and T::foo would be selected before <&T>::foo if there are any potentially valid trait impls for T. [playground] ↩︎

I accidentally kind of buried a lede in my previous post. To directly answer the OP question, you need to look into prehistory a bit. RFC 241 includes a section:

When auto-borrowing was removed, this reasoning difficulty was cited as a major motivator:

Code readability does not necessarily benefit from autoref on arguments:

let a = ~Foo;
foo(a); // reading this code looks like it moves `a`
fn foo(_: &Foo) {} // ah, nevermind, it doesn't move `a`!

let mut a = ~[ ... ];
sort(a); // not only does this not move `a`, but it mutates it!

Having to include an extra & or &mut for arguments is a slight inconvenience, but it makes it much easier to track ownership at a glance. (Note that ownership is not entirely explicit, due to self and macros; see the appendix.)

This RFC takes as a basic principle: Coercions should never implicitly borrow from owned data.

[...]

In today’s Rust, ownership transfer/borrowing is explicit for all function/method arguments. It is implicit only for:

  • self on method invocations. In practice, the name and context of a method invocation is almost always sufficient to infer its move/borrow semantics.

  • Macro invocations. Since macros can expand into arbitrary code, macro invocations can appear to move when they actually borrow.

This can let us infer that at the time autoref was directly tied to &[mut] self receivers for method invocation. I'd hazard a guess that the ability to select <&T>::method on a receiver of type T was not a designed capability but was just a side effect of the implementation choices. Notably, the self lookup process was described as some number of autoderef followed by a single autoref; it makes sense for that autoref to be for the Self to &Self coercion.

1 Like

[1]

Taking these together, do you mean:

Given t: T and f: fn(&U) and T not &U, f(t):

  • Acts like f(&*t) if T: Deref<Target = U> (or &****t as required etc)
  • Else acts like f(& { t }) if T = U
  • (Else doesn't compile or whatever, not going to attempt being exhaustive)

So for example,

fn g<T>(_: &T) {}

fn ex(bx: Box<i32>) {
    g::<i32>(bx);      // Doesn't drop `bx`: `g(&*bx)`
    g::<Box<i32>>(bx); // Drops `bx`:        `g(& { bx })`
}

I.e. where things drop depends on probing types for Deref impls?


  1. I've seen people want that, and I think it has the same "moved it? oops guess I'll clone" hazards as P: AsRef<Path>. I'm willing to be convinced it's fine, but that's also not the actual point of this reply, hence hiding this in a footnote. ↩︎

That is an accurate description of the potential changes. As you note, together they produce an undesirable result. But to be clear, while I do claim I could support either change, I'm undecided on whether I think they're worth making. They each imply an incompatible statement about how the ownership semantics work, so both will break someone's mental model.

1 Like

Thanks @CAD97 @quinedot I see how auto(de)ref for recievers in ufcs can be problematic.

To be honest I'm not able to get my head around everything (especially the part about place & value expressions), but I have a question that doesn't seem to have been answered, that is what you think about a flag to manually enable auto(de)ref (Edit: it seems you aren't in favor of it since you're in favor of cancelling auto-borrows).

And thanks @CAD97 for telling some history about it.