I'm writing a bit of code that can either output to stdout or to a file. Based on some external condition, I instantiate the file or stdout and then create a trait object from the reference to the appropriate item:
use std::{io,fs};
fn write_it<W>(mut w: W) where W: io::Write { }
fn main() {
let mut stdout;
let mut file;
let t: &mut io::Write = if true {
stdout = io::stdout();
&mut stdout
} else {
file = fs::File::create("/tmp/output").unwrap();
&mut file
};
for _ in 0..10 {
write_it(t);
}
}
This works fine, until I try to call write_it multiple times. That will fail, as t is moved into write_it and thus is not available on subsequent iterations of the loop:
<anon>:18:18: 18:19 error: use of moved value: `t`
<anon>:18 write_it(t);
^
note: `t` was previously moved here because it has type `&mut std::io::Write`, which is non-copyable
I can work around it by adding another layer of indirection:
let mut t: &mut io::Write;
write_it(&mut t);
But this seems like it could be potentially inefficient. Is it actually inefficient? Is there a cleaner way of writing this code?
You'll need to explicitly reborrow:
for _ in 0..10 {
write_it(&mut *t);
}
One often sees this happen implicitly, but it is not in this case because write_it takes a raw generic, W, and the compiler only implicitly reborrows a &mut when used in a place that is expecting a &mut. E.g. if it was
fn write_it<W: ?Sized + Write>(w: &mut W) { ... }
your code works fine, since explicit &mut in the type of the argument will ensure that the compiler will implicitly reborrow with a shorter lifetime (i.e. the &mut*).
Cases like this demonstrate that &mut does in fact move ownership, the implicit reborrowing often disguises it in favour of improved ergonomics.
As for performance of the version with the extra reference: the speed of a &mut (&mut Write) is likely to be indistinguishable from a plain &mut Write: the virtual call will usually be much more expensive than dereferencing the &mut.
Furthermore, the aliasing guarantees of &mut means the compiler is very free about how interacts with a &mut: e.g., depending on the internals, it may load the two words of the &mut Write from the pointer into registers once at the start of write_it and then write any changes back at the end. This is legal because being a &mut means that there's nothing else that can mutate that memory.
Lastly, at the moment, a "large" value like a &mut Write is passed via a pointer; essentially the same as a &mut &mut Write on the machine. The assembly for both the &mut *t and &mut t versions both start (literally the only difference I can see is the names of the Ltmp... labels):
_ZN8write_it20h2919620193267806634E:
.cfi_startproc
cmpq %fs:112, %rsp
ja .LBB4_2
movabsq $72, %r10
movabsq $0, %r11
callq __morestack
retq
.LBB4_2:
pushq %r14
.Ltmp116:
.cfi_def_cfa_offset 16
pushq %rbx
.Ltmp117:
.cfi_def_cfa_offset 24
subq $56, %rsp
.Ltmp118:
.cfi_def_cfa_offset 80
.Ltmp119:
.cfi_offset %rbx, -24
.Ltmp120:
.cfi_offset %r14, -16
movq (%rdi), %rsi
movq 8(%rdi), %rax
...
The two movqs at the end are loading the two words of the &mut Write trait object into registers.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With