Hello.
I am new to Rust and want to have variable that could be either stdout or stderr.
In CPP both stdout and stderr are ostream, so I can have reference to it, but in Rust types are different (Stderr and Stdout) and do not provide static lifetime (which I believe was done to prevent concurrent problems) so I can't have static livetime reference
So, the following code doesn't work
let mut stream = if env::args().len() > 2 {
std::io::stderr()
} else {
std::io::stdout()
};
stream.write("abc".as_bytes()).unwrap();
because branches return different types.
This approach doesn't work either
let stream:&dyn Write = if env::args().len() > 2 {
&std::io::stderr()
} else {
&std::io::stdout()
};
because object dies at the end of the branch and I can't have reference to it.
I can solve it with:
Boxing (which sounds strange, since I do not want to touch the heap)
Custom enum which could be either Stderr or Stdout, but too much boilerplace
Use static polymorphism (template function or struct that accepts Write), but again too much code.
What are the reasons for having different types for stderr and stdout? What is the right approach to solve it?
It may not be very practical for beginners but if you find yourself running into this problem often, I suggest using CPS, it gives a simple enough solution with no boxing. This package makes writing CPS code very nice
It's not typical to desire to be polymorphic over stderr and stdout, but at the same time avoid generalization to arbitrary Write types. With regards to the implementation, since the streams behave differently, it is easier to write them correctly if they are indeed different types rather than a single type with an enum inside. In the latter case every method on stdout and stderr would have to branch on the contents of this enum. The branching itself would be insignificant compared to syscalls, but the readability would be reduced. It would be easier to mess up variant handling and use unbuffered or line-buffered IO in the wrong place.
If you want to understand "how does stdio buffering work", nowadays you need just to look at the type. If both variants would be the same type, you'd have to carefully read all methods. Different types also allow to write different documentation for both types and their methods, reducing end-user confusion.
Note that if you want to coerce both to a trait object in a somewhat ergonomic way, you can box them:
let mut stream: Box<dyn Write> = if env::args().len() > 2 {
Box::new(std::io::stderr())
} else {
Box::new(std::io::stdout())
};
stream.write(stuff)?;
let stream = if env::args().len() > 2 {
Either::Left(std::io::stderr())
} else {
Either::Right(std::io::stdout())
};
Either is the approach of using one concrete type for both cases, and it has an extra boolean to check which one to use.
Rust is a bit obsessed with zero-cost abstractions and code optimized for singular specific cases, so if it doesn't have to have extra booleans and ifs, it doesn't, and leaves it to users to add those "overheads".