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 movq
s 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