Refs passed as argment are moved or untouched?

&mut references aren't Copyable, so you normally wouldn't expect code like this to work; the first push call should consume the reference leaving it unavailable for the second call:

fn append2(v: &mut Vec<u32>) {
    v.push(4);
    v.push(7);
}

Because this is a common pattern, &mut is treated specially by the compiler. The above code is silently turned into this behind the scenes:

fn append2(v: &mut Vec<u32>) {
    (&mut *v).push(4);
    (&mut *v).push(7);
}

The &mut *v is the reborrow: you're making a brand-new &mut that points to the same place as the old one, and the borrow checker prevents you from using the original until the new one you made is destroyed.

9 Likes

My understanding is: This happens because reborrowing is a coercion, and for coercions to happen the source and target types need to be known. When passing something as an argument, and the argument is generic, the target type is not known.

2 Likes

Yes, it is the reason.
However, it can be realized when compiler concretizes the template function and generates different code .
It is a bug of compiler.

If you believe it is a bug, why not support a bug report?

I don'r know how.....:slight_smile:

Rust's generics don't work that way, type inference runs on the generic function, not on its monomorphizations.

This is a known limitation, there should already be bug reports about this.

To submit a bug report against the compiler, go to its GitHub issues page and press the green “New Issue” button on the right side just above the list of known bugs.

Rust type checks before monomorphization, and this is intentional, not a "bug". The reason for it is that once a generic function type checks, it's guaranteed to compile (and work as intended) for all possible instantiations.

If type checking depended on the monomorphized code, it would be possible to write a generic function that would compile at first, but it would be rejected for certain instantiations that otherwise satisfy the trait bounds and other constraints. This happens all the time with C++ templates, for example, and it is highly undesirable because it makes generic code a lot less robust, testing harder, and basically involves testing a bunch of different instantiations "exhaustively" in order to ensure a clean compile.

5 Likes

Can't understand what is type checks and monomorphization

If you mean type checks is the process to analyse the template function itself, making sure all using of T satisfies T: trait1 + trait2 ... etc. And monomorphization is replace T with specific type.

Then I think both the 2 places don't concern the T's re-borrowing.

after type checks and monomorphization, a normal function is generated and there is no different with another typing-by-hand normal function.

And we know the re-borrowing occurs when calls a typing-by-hand normal function at the position passing arguments to it, then why it can't happen when calls a template-generated normal function?

1 Like

It's not that it can't – it would indeed be possible to analyze the already-generated (monomorphized) code instead of analyzing the generic code. C++ does that.

However, it comes with all sorts of bad surprises, as I already mentioned in my previous reply. Hence, it results in more robust code to only check the generic code, therefore Rust chose to do that.


And no, type checking isn't only concerned with resolving trait bounds. Borrows are absolutely part of it in Rust, because borrowing is built into the type system.

It's not a bug. This would fundamentally change the language, so this should be an RFC or pre-RFC instead of a bug report.

I am curious about that why it need analyse the generic code or monomorphized code. It just need to add re-borrowing when calls that function, like what has been done for a typing-by-hand normal function.

Seems it happens when analyzing the caller.

  1. Reborrowing can be added only after the types are known.
  2. Types are known only after monomorphization.
  3. Monomorphization occurs after typechecking.

What point in this list makes you feel it's a bug?

2 Likes

why Reborrowing can only be added to callee function, why not caller?

In the caller, the compiler knows whether or not to insert a reborrow before actually running the borrow checker. Here's a post I wrote a while ago on a related topic. Reborrowing can only happen at a coercion site where both the actual type A and the expected type E are of the form &'_ mut T, with (potentially) different lifetimes in the '_ slot. If E is generic, the compiler will simply choose E = A, and the value will be moved instead of reborrowed. This matches how other types of coercions work, and is why you don't have to specify types at every possible coercion site. The compiler will not choose E as some other type than A in order to allow a reborrow to happen.

In your earlier example you can make the test1 call compile by writing test3::<&mut _>(xref), which hints to the compiler to use a fresh lifetime for monomorphizing test3, so xref will be reborrowed instead of moved. (Of course you could also just explicitly reborrow: test3(&mut *xref).)

Could we invent a new rule that allows reborrowing everywhere? Maybe (I'm not convinced there isn't some scenario where you need to move). But that would be more complicated than the current situation, not less. Would the language be just as expressive and less complicated without the implicit reborrow rule? Yeah. But it would mean you have to use &mut * a lot and that would also be hard to explain to new users. The reborrowing rule makes sense in the vast majority of cases. Anyway, that would be a different language. (If you're inventing a new language based on Rust but without implicit reborrowing, consider also eliminating . for method call syntax; it makes things way simpler.)

tl;dr Yes, implicit reborrowing is a little quirky in how it interacts with generics, but it's useful in other ways.

4 Likes

It's a (current) limitation of type inference and dropck the module in the compiler that checks for ownership and moves, I'll call it moveck: Rust special cases some ownership / move rules for references, but in a generic / inferred type context, Rust may be unable to spot that it is dealing with references, thus not applying the special-cased rules

For instance,

if you feed a mut_ref: &'lifetime mut T to a function such as drop::<_>, moveck seems to be triggered before inference is resolved, thus stating that the value has been moved, and thus that the original binding is invalidated unless it is Copy.

But if you feed such mut_ref to drop::<&'_ mut _>, dropck will see that even though the generics have not been inferred yet, this is dealing with a &mut ref, and thus a reborrow applies.

  • At that point, the only way for mut_ref to become unusable is if it gets reborrowed for exactly its own lifetime (but that's for borrowck to deny, if that were the case, after the type inference pass).

But in this case, since there is no constraint on the lifetime parameter of that drop function, inference can choose

  • _ = T,

  • and instead of '_ = 'lifetime, a special rule for lifetime inference gets to kick in, yielding:
    '_ = 'x where 'lifetime : 'x

hence allowing a (shorter) reborrow ('x being the lifetime of the reborrow).

1 Like

If in doubt, spell it out.

This whole conversation has become so convoluted that I have long since lost track of what the problem is.

To my naive newbie Rust mind, and from my experience in C, C++ and other languages, the lifetime of anything is not determined by any tick mark decorations you put on types and declarations etc. It's determined by where it is created, where it is destroyed and how it is passed around. Typically some scope unless 'smart' pointers are involved.

As such, lifetime tick marks would be everywhere if one wants to specify types rigorously.

Luckily Rust can infer lifetimes in many cases so we don't have to have that lifetime tick mark syntactic noise everywhere.

Sometimes though Rust cannot guess what we are thinking. For whatever reason. If I understand correctly the Rust borrow checker has been getting smarter about this over time.

Well, OK, spell it out. It's not a bug. It's a bonus whichever way you look at it.

Or what am I missing here?

Wait, what does dropck have to do with all this? This isn't about destructors, it's about the interaction of type inference with coercions.

Yeah, dropck may not be the name of the module at play here, it would be something like moveck, but I don't know the official name. I remember that some ownership-related features are handled by dropck, wven if not all of them are about drops per se.

I will edit my post to remove that unclear part anyways :+1:

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.