Fn return impl Trait with different types?

I've just hit an unexpected bump. While working on this refactoring I created a trait and 3 structs that implement it. then I've written a function that returns impl Trait and wanted to return an instance of one of those struct depending on the parameters given to that function.

Sadly that fails to compile on the second return type, complaining it's not the same type as the first one (duh. I told you I'm returning an implementation of a trait, not the same one every time..)

I've found this post that seems to talk about that limitation of the "impl Trait" construct.

I haven't read the complete thread, but scrolling to the end looks like the suggested workaround in 2019 was to use this crate

Is this still the preferred solution in 2024 and does it fit my situation (as described above)?

UPDATE: reading a bit of documentation for the auto_enums crate, it doesn't seem I can use it for my current problem. If I understood it correctly, it only support a pre-defined list of common traits of common crates.

-> impl Trait doesn't ever give you new flexibility in what you return.[1] What it does is hide a type from your function's callers; you still have to make a choice about how to represent your combination of different types as a single type. That can be an enum (perhaps one already defined in a library), dyn Trait, or something else.

The appropriate representation will depend on the characteristics of the trait, the types, the function, and how the return value is going to be used. So, tell us more about your specific situation: what is the function, and what is the trait? Not what their purpose is, but what their exact definitions are.


  1. except for returning unnameable function and future types, by avoiding needing to name them ↩ī¸Ž

3 Likes

thank you for your reply! For now I've manually written an enum to complement my trait, but to my taste this looks like a lot of redundant boiler plate. Maybe you have any suggestions how to reduce the boiler plate? That Ether crate sounds interesting, I'll have a look at that next.

As I said, it depends on the details.

  • Sometimes Box<dyn Trait> or &dyn Trait is appropriate and you can avoid the boilerplate.
  • Sometimes you can do something clever so that the two different types are already one type and you don't need any special wrapper.

Please, share a concrete, preferably compilable, code sample that includes a trait, two types and a function that wants to return two implementations, and then we can discuss ways to improve it.

2 Likes

oh sorry, I thought I've posted that link to my source on the last post, here it is:

This has

  • a trait DataSender
  • 4 structs that implement it
  • 1 enum that joins those 4 structs into a type so I can return it from
  • the function get_sender
  • the function sender_loop that calls get_sender and calls the trait's function on the returned enum (repeatedly)

I'd prefer a solution that doesn't require me to manually create that DataSenderEnum which feels like pure boilerplate to my eyes.

This one's tricky because the trait contains an async fn. I would keep it as it is. If it didn't, I would suggest Box<dyn DataSender>. I would say, keep the enum.

If you really want to be able to eliminate it, you can use https://docs.rs/async-trait/ which transforms the trait into a fully type-erased form, allowing you to use Box<dyn DataSender>. This costs an extra Box allocation per send() call, which is probably insignificant.

1 Like

okey, I will keep it as is then. The send will be called quite often and this program should run on very limited hardware like openwrt routers where I don't want to slow down the luci web interface which already feels slow in some cases.

Though are you sure it would be an additional allocation? wouldn't it just replace the allocation of the enum type?

Then you should prefer the enum and take care everywhere about how many allocations and other operations you are doing.

Box creates a heap allocation. Instantiating an enum doesn't.

2 Likes

Allocating an extra Box won't in itself cause noticeable slowdown. While a dynamic allocation is traditionally regarded as "expensive" compared to primitive operations (eg. arithmetic or moving data around), I don't think you can find commodity hardware today that would be slow enough to cause even a frame to be skipped just because of an allocation.

If you are writing a UI, then you have something like a couple milliseconds or tens of milliseconds (depending on the framerate) to perform computations between two updates of the rendering loop. A single dynamic allocation doesn't take several milliseconds, not even on low-end microcontrollers.

1 Like

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.