Narann
March 27, 2020, 5:04pm
1
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!
alice
March 27, 2020, 5:10pm
2
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
ONiel
March 27, 2020, 5:20pm
3
#![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
alice
March 27, 2020, 5:22pm
4
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
Narann
March 27, 2020, 5:52pm
6
Thanks all!
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
netns
March 27, 2020, 7:57pm
7
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
Narann
March 27, 2020, 9:13pm
8
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.
alice
March 27, 2020, 9:15pm
9
Well they wont both be created, but it's true that there will be allocated space for both.
system
Closed
June 25, 2020, 9:15pm
10
This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.