Beginner question: how to port a class hierarchy

I have some C++ code that implements a UI library. There is a root View class with subclasses like Button, VStack, TextField, etc. The View base stores certain things like it's "frame" (rectangle for location in parent view or window). So some of it's functions are non-virtual. Then there are certain virtual functions like layout and render that all the classes implement. Classes like VStack contain std::vector<View*> children.

I read that Rust has no inheritance, and that composition is another way to do this. But in this case it seems like inheritance is more straightforward. So I'm not sure how I would port parts of this to Rust. I guess there will be a View trait that the various structs can implement. That replaces the virtual functions. But what about the common implementation parts, like all View's having a frame: Rectangle. Would you just repeat it in each struct? Even if it was at the same offset in each struct, would I then need a trait function like get_frame to access it if I had something like std::vector<View*> (whatever the equivalent in Rust is).

Rust truly has no implementation inheritance so you really do need to switch to composition. It is not unusual that an existing design cannot be ported to Rust. This earlier thread may help:

4 Likes

Yes, Rust has no inheritance, but has Box<dyn Trait>.
If you just convert SubClass to ParentClass in Rust, then Box<dyn Trait> is a good choice.
But if you want cast ParentClass to SubClass, downcast_ref in std::any module may help.

std::any - Rust


When I prepare write AST for parser,
first time I use Box<dyn ASTNode> for it, just like what I do in C++, but quickly I find out code for ParentClass to SubClass is too hard. Then I use a enum ASTNode to handle all the cases...

1 Like

That's probably a dyn View or enum situation. Here's a short blurb about some of the considerations. The choice may come down to whether or not you know the complete set of implementers.

If you use a View trait which inherently needs access to the Rectangle, you would have something like a getter method as part of the trait, yes. If every trait View implementer is its own independent type, yes, this also probably this means that they all have a field: Rectangle field.[1] It need not have the same name or be at the same "offset".[2] (It need not even exist; maybe some widget is always zero-sized at offset 0,0 or something.)

Alternatively to every implementer being an independent type, however, you could use generics with composition.

pub struct ViewWidget<Flavor> {
    frame: Rectangle,
    widget: Flavor,
}

pub struct Button {
    text: String,
    // ...
}

impl View for ViewWidget<Button> { /* ... */ }

// Or maybe:
//
// pub struct ButtonInner ...
// impl View for ViewWidget<ButtonInner> ...
// pub type Button = ViewWidget<ButtonInner>;

Then you can

// No extra bounds on `T`, though maybe you would want/need
//     `where ViewWidget<T>: View`
impl<T> ViewWidget<T> {
    fn non_virtual_method_from_cpp(&self) { /* ... */ }
}

and maybe you don't need the Rectangle getter on the trait anymore.[3]

These aren't the only possibilities; just a short example of what you might consider.


  1. Rust doesn't have fields in traits either. ↩︎

  2. Offsets don't matter unless you're using an explicitly specified layout (like #[repr(C)]) and unsafe -- don't do that here (it's something you might do for FFI or the like). ↩︎

  3. If you use dyn View, you probably still need it. If you use enums instead, you might not. Though if other generic consumers of the trait need to get the Rectangle, you still might need it for that reason. ↩︎

2 Likes

I also have an ASTNode hierarchy. Did your enum have repeated fields, which would have been in the base class in C++? For example:

enum ASTNode {
    If { loc: SourceLocation, test: ASTNode, ... },
    BinOp { loc: SourceLocation, lhs: ASTNode, rhs: ASTNode },
    VarRef { loc: SourceLocation, name: Symbol }
    ... 
}

Or I guess you could follow the answer from quinedot, and do something like:

struct ASTNode<Flav> {
    // fields common to all ASTNodes
    loc: SourceLocation,
    ...
    flavor: Flav    // :) 
}
type IfAST = ASTNode<IfPart>
...

I use the first way, some repeat fields but clear

enum ASTNode {
    If { loc: SourceLocation, test: ASTNode, ... },
    BinOp { loc: SourceLocation, lhs: ASTNode, rhs: ASTNode },
    VarRef { loc: SourceLocation, name: Symbol }
    ... 
}
1 Like