4

There are two ways to "combine traits" in Rust, I believe. I'm wondering what the differences are.

To be concrete, let's say the two traits are Beeps and Honks

trait Beeps { fn beep(&self); }
trait Honks { fn honk(&self); }

A new empty trait can be formed, as explained here:

impl<T: Beeps + Honks> BeepsAndHonks for T {}

For concreteness, an implementation would just be this:

struct Obj {}
impl Beeps for Obj { fn beep(&self) {} }
impl Honks for Obj { fn honk(&self) {} } 

The combined trait can be used like so

fn factory() -> Box<dyn BeepsAndHonks> {
    Box::new( Obj {} )
}

The other way to achieve a "combined trait" is via suptertraits:

trait BeepsAndHonks2: Beeps { fn honk(&self); }

This uses Beeps as a base and layers a honk on top. (Sorry for all the silly beeping and honking.)

When I don't own Beeps and Honks then the new empty trait is the only way to do this (I believe). Otherwise I can do either new empty trait or supertrait. Structurally the two approaches are different, but under the hood there is no difference, e.g. regarding number of vtable lookups, is that correct?

Since traits clearly can be combined, what speaks against allowing the Beeps+Honks syntax, e.g. fn factory() -> Box<dyn Beeps+Honks>?

mcmayer
  • 1,931
  • 12
  • 22
  • One thing I just noticed: There are [Object Safety](https://doc.rust-lang.org/reference/items/traits.html#object-safety) requirements for base traits. For example, it seems that `Iterator` does not fulfill these requirements. – mcmayer Nov 28 '22 at 09:11
  • Note that there is a third option, that you didn't mention, which may fit your requirements. See [the playground](https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=c0e24cfe5bd8c4727ea1114784b0222e). It's a combination of your two solutions. – jthulhu Nov 28 '22 at 09:44
  • There are trait aliases `trait Trait2 = Trait0 + Trait1;`, but they’re not suitable for trait objects. The best way is to create a supertrait `trait Trait2: Trait0 + Trait1 {}` `impl Trait2 for T {}` – Miiao Nov 28 '22 at 09:49
  • @jthulhu Yes I see. Now it's even less clear to me whether these are just three different ways to do the same thing. – mcmayer Nov 28 '22 at 10:19

1 Answers1

4

These two ways are not equivalent. Let's see what different options actually mean:

Generic implementation of supertrait

Your first example:

trait Beeps { fn beep(&self); }
trait Honks { fn honk(&self); }

trait BeepsAndHonks {}
impl<T: Beeps + Honks> BeepsAndHonks for T {}

struct Obj {}
impl Beeps for Obj { fn beep(&self) {} }
impl Honks for Obj { fn honk(&self) {} }

Here you provide implementation of the BeepsAndHonks trait for any trait that have both Beeps and Honks type. This is convenient not have to write implementation for every such type, but note that you can have a type implementing BeepsAndHonks that cannot neither beep nor honk:

struct AnotherObj {}
impl BeepsAndHonks for AnotherObj {}

Supertraiting Honks

Your second example:

trait BeepsAndHonks2: Beeps { fn honk(&self); }

is different from the first. In this case you can have a type a that implements Beeps or both Beeps and Honks together, but not only Honks as allowed by the first example.

Supertrait with bounds

You can prevent the problem in the first example by requiring BeepsAndHonks to also implement related traits:

trait BeepsAndHonks: Beeps + Honks {}

Now AnotherObj cannot exist. If you add Beeps and Honks implementations for it, it will have two conflicting implementaions of BeepsAndHonks (one manual and one generic from the first example). And if your don't provide them trait bounds are not satisfied.

Maxim Gritsenko
  • 2,396
  • 11
  • 25