Stderr and stdout

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:

  1. Boxing (which sounds strange, since I do not want to touch the heap)
  2. Custom enum which could be either Stderr or Stdout, but too much boilerplace
  3. 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?

Thank you.

You can declare some variables outside the if to hold the io objects.

7 Likes

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

1 Like

@quinedot thanks, that works! We also could do like

    let mut stderr: Stderr = std::io::stderr();
    let mut stdout: Stdout = std::io::stdout();
    let mut stream: &mut dyn Write = if env::args().len() > 2 {
        &mut stdout
    } else {
        &mut stderr
    };
    write!(stream, "hello").unwrap();

but I still can't get why do we have different types for streams:)

@yuriy0 thank you! I will look at it

My way only initializes one... but I think it has about zero cost so it doesn't really matter here.

I don't know the history of the type separation, but they do behave differently (e.g. with regards to buffering).

1 Like

If you look at the source code, std::io::Stdout contains a bufferred writer while std::io::Stderr doesn't.

4 Likes

Presumably you could define an enum to hold the two options, and then use the enum as the single type?

That's what I would do in Haskell.

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)?;
1 Like

Note that your version initializes both variables. The one that @quinedot suggested doesn't do any extra work.

Another alternative is using the either crate:

     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".

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.