Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Can I pass the same mutable trait object in multiple iterations of a loop without adding indirection?

Tags:

rust

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?

like image 712
Shepmaster Avatar asked Oct 20 '25 02:10

Shepmaster


1 Answers

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.

like image 53
huon Avatar answered Oct 22 '25 04:10

huon



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!