My program has a command line flag that allows specifying the path of an output file. If the flag isn't specified, I want to write to stdout.
I came up with the following code snippet using trait objects:
use std::io;
use std::fs::File;
use std::io::{BufWriter, Write};
fn main() {
//let out_path = Some("/tmp/out");
let out_path: Option<&str> = None;
let output: &Write = match out_path {
Some(path) => &File::create(path).unwrap(),
None => &io::stdout(),
};
let mut writer = BufWriter::new(output);
write(&mut writer);
}
fn write(out: &mut Write) {
writeln!(out, "hello world").unwrap();
}
This doesn't work, though. It fails with:
error[E0277]: the trait bound `&std::io::Write: std::io::Write` is not satisfied
--> src/main.rs:13:22
|
13 | let mut writer = BufWriter::new(output);
| ^^^^^^^^^^^^^^ the trait `std::io::Write` is not implemented for `&std::io::Write`
|
= note: required by `<std::io::BufWriter<W>>::new`
I'd like to understand why that is. I noticed that using Box<Write>
instead of &Write
works as expected, so I'm going with this solution for now. Still, I'm curious. Do reference trait objects just not work this way? How are they meant to be used then?
The std::io::Write
trait has methods that take &mut self
so calling them requires an exclusive borrow of the thing implementing Write
. When you have &Write
it is a shared borrow so you can hand it around to as many places as you like, but none of those places can call Write
's &mut self
methods so &Write
does not implement Write
.
Note that &mut Write
does implement Write
. In particular, the standard library contains this impl:
impl<'a, W: Write + ?Sized> Write for &'a mut W
In addition to what @dtolnay said, you have other problems with that snippet. Namely, you're attempting to create references to temporaries (the File and stdout) - the compiler won't allow that if you got to that point. The easiest way to facilitate the trait object here is with boxing both types, and creating a Box<Write>
, as you found out. This will own either the File or Stdout values. The stdlib has an impl<W: Write + ?Sized> Write for Box<W>
, which then lets you use Box<Write
as Write.
Somewhat tangentially, the stdlib also has impl<'a> Write for &'a File
, which I find somewhat surprising. I wouldn't have expected that having mutable borrows to a shared File reference to be allowed for Write, but it's there.
By the way, here's an approach for you that doesn't involve trait objects/boxes. This assumes that, as your post says, you either write to a file or to stdout based on cmdline args.
enum MyWriter {
ToFile(File),
ToStdout(io::Stdout)
}
impl Write for MyWriter {
fn write(&mut self, buf: &[u8]) -> std::result::Result<usize, std::io::Error> {
use self::MyWriter::*;
match *self {
ToFile(ref mut f) => f.write(buf),
ToStdout(ref mut out) => out.write(buf)
}
}
fn flush(&mut self) -> std::result::Result<(), std::io::Error> {
use self::MyWriter::*;
match *self {
ToFile(ref mut f) => f.flush(),
ToStdout(ref mut out) => out.flush()
}
}
}
fn main() {
let out_path: Option<&str> = None;
let output = match out_path {
Some(path) => MyWriter::ToFile(File::create(path).unwrap()),
None => MyWriter::ToStdout(io::stdout())
};
let mut writer = BufWriter::new(output);
write(&mut writer);
}
fn write(out: &mut Write) {
writeln!(out, "hello world").unwrap();
}
// I'd make write() generic, rather than accept trait objects
fn write_generic<W: Write>(out: &mut W) {
writeln!(out, "hello world").unwrap();
}
2 Likes
These answers are really helpful. I appreciate the effort, thank you!