Is it possible to avoid making an entire struct generic for an optional field?

use std::sync::mpsc::{Sender, Receiver};

pub struct Transport<T> {
    sender: std::sync::mpsc::Sender<T>,
    recver: std::sync::mpsc::Receiver<T>,
}

pub struct App<T>{
    name: String,
    transport: Option<Transport<T>>
}
impl <T> App <T> {
    pub fn new(name: String) -> App<T> {
        App { name, transport: None }
    }
    pub fn with_transport(name: String, t: Transport<T>) -> App<T> {
        App { name, transport: Some(t) }
    }
}
fn main()
{
    let name = String::from("myapp");
    let app = App::new(name);
}

(Playground)

This results in an error that app needs a type, however, I think it would be convenient if the user didn't have to specify a type unless they were using a transport. Ideally, the user would call App::new() to create an app without a transport and App::with_transport() to create an app with a transport. Is this possible or is there a better way?

To give you an example for why you need to provide a type parameter, what would happen if the layout of a std::sync::mpsc::Sender<T> changed depending on the T?

Imagine if it stored a couple elements in-line to allow bulk sending. If that were the case a Sender<u8> might be 8 bytes long while a Sender<usize> could be 16 (the numbers aren't important here). If that were the case then Transport<T>'s size would change, causing App<T>'s size to also change.

Now the compiler runs into a problem... If it doesn't know what T is being used in App<T> then how much space should it set aside for the app variable?

Sure there might be some concrete cases where changing the type parameter won't result in layout changes (e.g. Box<T> is always the size of a pointer, regardless of the T), but in general it's not possible to know a type's layout until you substitute in all the type parameters.

There are other examples (e.g. backwards compatibility and auto traits like Send or Sized), but the layout ones were the first to come to mind.

You can pick some fixed type for this case.

pub fn new(name: String) -> App<()> {
    App { name, transport: None }
}

Here we use the type () as the transport item rather than using a generic parameter. Someone can then store it in their struct without generics:

struct HasAnApp {
    inner: App<()>,
}

You can even define a type alias for it:

type AppWithoutTransport = App<()>;

Of course you could use any type here, e.g. App<String> or App<i32>.

1 Like

and if you use an enum without variants as the type for absence of T, then it literally won't take any space and code for it will be removed.

This prints 0, because Rust statically understands that Transport can't exist in this situation:

struct Transport<T> {
    x: T,
}

struct App<T> {
    transport: Option<Transport<T>>,
}

enum Nope {}

fn main() {
    println!("{}", std::mem::size_of::<App<Nope>>());
}

OTOH if you want to get rid of T from App<T>, then you'd have to put transport behind dyn, e.g.

transport: Option<Box<dyn Transportable>>;

and make a Transportable trait for functionality you need from it.

  use std::sync::mpsc::{Sender, Receiver};

  pub struct Transport<T> {
      sender: std::sync::mpsc::Sender<T>,
      recver: std::sync::mpsc::Receiver<T>,
  }

+ enum NoPayload_ {}
+ pub struct NoPayload { _private: NoPayload_ }

- pub struct App<T>{
+ pub struct App<T = NoPayload>{
      name: String,
      transport: Option<Transport<T>>
  }

- impl<T> App<T> {
+ impl App {
      pub fn new(name: String) -> Self {
          App { name, transport: None }
      }
+ }
+ impl<T> App<T> {
      pub fn with_transport(name: String, t: Transport<T>) -> App<T> {
          App { name, transport: Some(t) }
      }
  }

This basically showcases what the comments above have suggested, so that:

fn main ()
{
    let app = App::new("myapp".into());
}

Just Works™, with optimal size, while

fn main ()
{
    let app = App::with_transport("myapp".into(), …);
}

also works :slightly_smiling_face:


Note that if you have been using an Option for this statically-known presence or absence of transport, then I recommend you remove the Option altogether:

pub struct NoTransport { _private: () }

pub struct App<T = NoTransport> {
    name: String,
    transport: T,
}