Traits for ducktyping?

TLDR; if I have a struct A { a: u32, b: u32} and an impl of a trait that returns a reference to inside that struct trait C { fn a() -> &u32;} that essentially is only pointers into the struct then does that all disappear in compiler optimisations?

My usecase is that I prefer very small interfaces so my utility code typically defines minimum interfaces/traits that it needs. Callers then provider typically richer code that is a superset of that interface. In dynamic languages with duck typing this works well.

For example, let's say I have a utility which "merges" overlapping objects that are otherwise identical. I might have:

pub trait Candidate<T> {
  fn get_start_range(&self) -> Option<&chrono:NaiveDateTime>;
  fn get_end_range(&self) -> Option<&chrono:NaiveDateTime>; 
  fn is_identical_excluding_range(&self, other: &T) -> bool;
}

pub fn reduce<T>(candidates: &Vec<Candidate<T>>) -> Vec<Candidate<T>> {
...
}

and then in my caller code I might have:

struct MarketOffer {
  start: chrono::NaiveDateTime,
  end: Option<chrono::NaiveDateTime>,
  product: ....,
  ...
}

impl Candidate for MarketOffer {
  fn get_start_range(&self) -> Option<&chrono:NaiveDateTime> {
    Some(&self.start)
  }
  fn get_end_range(&self) -> Option<&chrono:NaiveDateTime> {
    &self.end
  }
  fn is_identical_excluding_range(&self, other: &MarketOffer) -> bool {
    &this.product == other.product
  }
}

(forgive the errors, I've typed this by hand ;-)).

My question is what is the runtime cost for this? Will the compiler realise, and optimise away the noise so my reduce calls directly into my MarketOffer struct?

I hope that makes sense - thanks!

As long as you use traits for static dispatch (i.e. no dyn), you can expect that trait methods will be optimized in exactly the same way inherent methods and free functions are optimized.

1 Like

you mean fn a(&self) -> &u32;, right?

yes. Although, to make proper optimization work cross-crate, you’ll have to make sure to put #[inline] on the fn in the impl C for A, I think.

This doesn’t seem correct Rust code. Candidate<T> is a trait which makes &Vec<Candidate<T>> be outdated alternative syntax for &Vec<dyn Candidate<T>>; but you can’t directly put trait objects into a Vec, and also trait objects can have runtime overhead from dynamic function calls (harder to inline if the compile can't figure out the concrete type behind the trait object at compile-time). (Note that this kind of overhead is also omni-present in dynamic languages, so compared to those, Rust shouldn’t be worse even when using trait objects.)

OTOH, if you’re talking about a generic function, e.g. pub fn reduce<T, C: Candidate<T>>(candidates: &Vec<C>) -> Vec<C>, then the call to get_start_range, get_end_range, etc. can be inlined easily, and there’s never any overhead from the trait dispatch at run-time, either.

Even with trait objects, e.g. if the call to “reduce” is inlined, it is possible that there’s no overhead.

Also note that the type &Vec<…> (“shared reference to vec”) is discouraged in Rust, try to use slices as argument instead, so e.g.

pub fn reduce<T, C: Candidate<T>>(candidates: &[C]) -> Vec<C>

Thanks @steffahn and @H2CO3 (and I hope my typed in code didn't make your eyes bleed too much ;-))

It’s often useful to turn examples into real rust code, e.g. in the playground.

Here’s your example with a few changes: Rust Playground

The playground also offers Rustfmt and Clippy. When you do this first, you’ll get properly formatted code without syntax errors and your question becomes clearer. If you run clippy on your code, it’ll even point out things like usage of &Vec<…>, too.

    Checking playground v0.0.1 (/playground)
warning: writing `&Vec<_>` instead of `&[_]` involves one more reference and cannot be used with non-Vec-based slices
 --> src/lib.rs:7:42
  |
7 | pub fn reduce<C: Candidate>(_candidates: &Vec<C>) -> Vec<C> {
  |                                          ^^^^^^^ help: change this to: `&[C]`
  |
  = note: `#[warn(clippy::ptr_arg)]` on by default
  = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#ptr_arg

warning: `playground` (lib) generated 1 warning
    Finished dev [unoptimized + debuginfo] target(s) in 0.94s

But the main advantage is spotting errors such as chrono:NaiveDateTime (single colon!), usage of this instead of self (in is_identical_excluding_range), the impl Candidate even though pub trait Candidate<T> has a generic argument (I’ve fixed that by removing the generic argument, you could also do e.g. impl Candidate<MarketOffer> for MarketOffer instead, or add a default pub trait Candidate<T = Self>, and the usage of Vec<Candidat<T>> missing the dyn and not compiling because of a trait object that’s not behind a pointer.

2 Likes

Good call - that makes better use of others time as well. I'm away from my computer and didn't think of the playground. Good call.

1 Like

Ah, well… if it’s a mobile device I could understand not using a playground; it doesn’t work too well on e.g. a phone (at least for me). It’s not entirely unworkable, but at least a bit of a pain.

1 Like

Wow but the playground has advanced since I looked at it years ago (I'm usually at my desk). It now shows the assembly - neat

See also Godbolt for your asm exploration needs. You may want to specify -C opt-level=3.

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.