Using traits for interface?

Hi, I'm sure I'm not using traits the right way.

All the tuto explains how to use traits, but not how solve the interface problem.

So here is my simple question: I have two renderer (OpenGL and Vulkan). I want to use an object calling function (interface functions) on them:

trait Renderer {
    fn new() -> Self;
    fn render();
}

struct RendererGl { a:u32 }

impl Renderer for RendererGl {
    fn new() -> RendererGl {RendererGl{a:1}}
    fn render() {println!("render gl")}
}


struct RendererVk { b:u32 }

impl Renderer for RendererVk {
    fn new() -> RendererVk {RendererVk{b:2}}
    fn render() {println!("render vk")}
}

fn main() {
    let mut renderer: Renderer = match "vk" {
        "gl" => RendererGl::new(),
        "vk" => RendererVk::new(),
    };
    renderer.render(); // expect "render vk"
}

So it's a simple Dog and Cat object accessed through an Animal type.

Any idea or documentation to achieve this ?

As I suspect this is not how it's supposed to be done in rust, what is the good approach.

Playrust link.

Thanks in advance and take care of yourself!

You can't really use traits as a type directly. You have to access them through some sort of pointer such as Box<dyn Trait> or &dyn Trait to use the trait as a type. Additionally using the trait as a type means that you are erasing the underlying concrete type, and this means the trait is not allowed to have certain kinds of methods that don't work when you erase the type.

This includes types with no self parameter, those that take or return self by value, and generic functions. So for example the constructor should not be part of the trait, and your render function should take a &self parameter.

1 Like
#![allow(unused)]


pub trait Renderer {
    fn render(&self);
}

struct RendererGl { a:u32 }
impl RendererGl {
    fn new() -> RendererGl {RendererGl{a:1}}
}

impl Renderer for RendererGl {
    fn render(&self) {println!("render gl")}
}


struct RendererVk { b:u32 }
impl RendererVk {
    fn new() -> RendererVk {RendererVk{b:2}}
}

impl Renderer for RendererVk {
    fn render(&self) {println!("render vk")}
}

fn main() {
    
    let mut renderer : Box<dyn Renderer> = match "vk" {
        "gl" => Box::new(RendererGl::new()),
        "vk" => Box::new(RendererVk::new()),
        _ => Box::new(RendererGl::new()), //Defining default behaviour
    };
    renderer.render();
}
  • Take the new() constructor out of the generic trait and implement them each separately (not obligated change but handy when code becomes more complex)
  • Match arms need to return the same type. Because you're trying to return or RendererVk or RendererGl you can wrap it in a box.
  • A match always needs to have an implementation for _. This is the default arm which gets executed in case no other match is found.

Rust playground.

1 Like

You don't need the casts, you can do this instead:

let mut renderer: Box<dyn Renderer> = match "vk" {
    "gl" => Box::new(RendererGl::new()),
    "vk" => Box::new(RendererVk::new()),
    _ => Box::new(RendererGl::new()),
};

And note that you need to put a dyn on the trait type.

1 Like

Speaking of dogs and cats: How can i do polymorphisme in rust - #4 by Yandros

1 Like

Thanks all! :heart:

The Box::new() method makes a lot more sense to me! I was guessing it was related to heap/stack allocation trouble. It's roughly the same with C++ world as you can't allocate a renderer on the stack as its size in not known at compile time.

Thanks again for the help peoples!

2 Likes

The proper approach is this:

fn do_main<T: Renderer>(renderer: Renderer) {
  renderer.render();
  // all other code
}

fn main() {
  match "vk" {
    "gl" => do_main(RendererGl::new()),
    "vk" => do_main(RendererVk::new())
  };
}

Or if you really don't want to monomorphize:

let mut gl;
let mut vk;
let mut renderer: &mut dyn Renderer = match "vk" {
        "gl" => {gl = RendererGl::new(); &mut gl},
        "vk" => {vk = RendererVk::new(); &mut vk},
    };
renderer.render();

You can also box the renderer but there's no need in this example.

3 Likes

It's an interesting approach, but it also mean both RendererGl and RenderVk structures will be stored on stack (in gl and vk), even if only one of them is actually initialized and used.

Well they wont both be created, but it's true that there will be allocated space for both.

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.