Using references to types in structs

I’m trying to understand how to include references to types inside structs:

use std::io::{BufRead};

struct Processor<B: BufRead> {
    reader: B
}

// If you uncomment p in ValidatorA, compilation fails because the size of
// ValidatorA can't be determined at compile time. Fair enough.
struct ValidatorA {
//    p : Processor<BufRead>    
}

// In ValidatorB, compilation fails for the same reason as above - but why?
// Aren't references a fixed size - a sort of fat pointer, if you will?
struct ValidatorB<'a> {
    p : &'a Processor<BufRead>
}

(Playground)

Errors:

   Compiling playground v0.0.1 (/playground)
error[E0277]: the size for values of type `(dyn std::io::BufRead + 'static)` cannot be known at compilation time
  --> src/lib.rs:16:5
   |
16 |     p : &'a Processor<BufRead>
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^ doesn't have a size known at compile-time
   |
   = help: the trait `std::marker::Sized` is not implemented for `(dyn std::io::BufRead + 'static)`
   = note: to learn more, visit <https://doc.rust-lang.org/book/second-edition/ch19-04-advanced-types.html#dynamically-sized-types-and-the-sized-trait>
note: required by `Processor`
  --> src/lib.rs:3:1
   |
3  | struct Processor<B: BufRead> {
   | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^

error: aborting due to previous error

For more information about this error, try `rustc --explain E0277`.
error: Could not compile `playground`.

To learn more, run the command again with --verbose.

What’s the general approach to be taken?

This issue is not related to references. This is caused by Processor<BufRead> code where BufRead is actually dyn BufRead, a dynamically sized type. You probably did not intend to use dynamic dispatch here, so you probably meant the following:

struct ValidatorA<B: BufRead> {
    p: Processor<B>    
}

In this case, ValidatorA is also a generic struct that passes its generic parameter to Processor.

If you don’t want ValidatorA to be generic, you can specify a concrete type for the field:

struct ValidatorA {
    p: Processor<BufReader<File>>    
}

And if you do want dynamic dispatch, you can do it with Box:

struct ValidatorA {
    p: Processor<Box<dyn BufRead>>    
}

Note that you can add #![warn(rust_2018_idioms)] to the top of your crate root so enable warnings involving the new dyn keyword. It will help avoid this kind of confusion.

2 Likes

OK, thanks. If I use a Box<dyn BufRead> the error does go away, but some things I’m still not sure about. For example, suppose I want an API where I pass in something that has a Read trait, apply buffering to it using BufReader and then process that thing which is a BufRead in my Processor. Can I define this API without any concrete types being defined? It appears not - it looks as if BufReader is generic and requires instantiation with a concrete type, rather than (as one might have expected) just taking a trait object which satisfies Read and then layering buffering on top of it. Can anyone shed some light on why the existing generic BufReader design is better than one where BufReader is not generic, but takes a reference to a Read and just uses it internally?

Playing with this to see how it might work, I naïvely got as far as

struct Validator {
    p : Processor<Box<dyn BufRead>>
}

impl Validator {
    fn maker(r : &dyn Read) {
        let br = BufReader::new(r);
    }
}

which predictably failed to compile, but with an error message which led to some head-scratching:

   |
13 | let br = BufReader::new(r);
   |          ^^^^^^^^^^^^^^ the trait `std::io::Read` is not implemented for `&dyn std::io::Read`
   |

Aside: You generally shouldn’t do this (except for file formats that are always read to EOF), because BufRead can read more bytes than necessary, and this side-effect can be observed if the caller passes in &mut R.

the trait `std::io::Read` is not implemented for `&dyn std::io::Read`

The error message is correct. Read is implemented by &mut dyn Read. (this is done explicitly in std) It can’t be implemented for immutable references because Read has mutating methods.

You should take <R: Read> instead of the reference in maker to avoid lifetime troubles. I would elaborate but my fingers are turning into icicles on my phone.

Yes, I see, though there are legitimate use cases for doing this (e.g. network protocol handling, language or data parsing) where you might read more than is necessary (e.g. you hit a protocol error or syntax error, and stop processing there with unprocessed data sitting in the buffer).

Not disputing that, but it does look odd, because it scans as “trait io::Read not implemented for io::Read” - and should that “for” be a “by”?

That would make maker a generic function, right? OK, but I was trying to explore the non-generic approach. I’m just learning, so I expect that quite a bit of trouble with lifetimes is in my immediate future :wink:

I thought your objective was to have the structs not be generic? You can have monomorphic structs with generic constructors.

There’s a matrix of options here:

  • Having maker take R: Read versus a trait object:
    • R: Read supports a wider variety of inputs
    • Directly taking a trait object might make it easier to avoid double indirection in some cases (e.g. you can avoid creating Box<dyn Read> from Box<Box<dyn Read>>).
  • Supporting lifetimes or requiring 'static:
    • Supporting lifetimes will require adding a lifetime to any type that isn’t generic over R
    • 'static won’t work for borrows

Here’s what some of the designs in that matrix look like:

(note: I changed the example to make it shorter; your exercise will be to map these techniques back to the case where Processor is generic and Validator is not)

trait object, 'static only:

struct Processor {
    br: BufReader<Box<dyn Read>>,
}

fn make_processor(r: Box<dyn Read>) -> Processor {
    let br = BufReader::new(r); // uses the existing Box<dyn Read>
    Processor { br }
}

<R: Read>, 'static only:

struct Processor {
    br: BufReader<Box<dyn Read>>,
}

fn make_processor<R: Read + 'static>(r: R) -> Processor {
    let b = Box::new(r);
    // performs an unsizing coercion on b:
    //             thin pointer:  Box<R> 
    //   coerces to fat pointer:  Box<dyn Read + 'static>
    let br = BufReader::new(b as _);
    Processor { br }
}

<R: Read>, any lifetime:

struct Processor<'a> {
    br: BufReader<Box<dyn Read + 'a>>,
}

fn make_processor<'a, R: Read + 'a>(r: R) -> Processor<'a> {
    let b = Box::new(r);
    let br = BufReader::new(b as _); // b gets an unsizing coercion here
    Processor { br }
}

Edit: Added some very important as casts that were missing. Without them, br gets created as BufReader<Box<R>>, which cannot be coerced to BufReader<Box<dyn Read>>. With them, the coercion is performed before the BufReader is constructed, so that it is already BufReader<Box<dyn Read>>.

1 Like

Thanks for that. Lots of food for thought!