3

I'm trying to implement the Rust equivalent code of the following C++ code which makes use of inheritance, but got stuck. This is my sample code:

class Vehicle {
public:
    double lat;
    double lon;
    double alt;

    double speed;
};
    
class CabVehicle : public Vehicle {
    
};
    
class PackerMoverVehicle : public Vehicle {
    
};
    
int main() {
    CabVehicle cv;
    cv.lat = 12.34;
    cv.lon = 12.34;
    cv.alt = 12.34;

    PackerMoverVehicle pmv;
    pmv.lat = 12.34;
    pmv.lon = 12.34;
    pmv.alt = 12.34;
}

How should this be written in Rust?

kmdreko
  • 42,554
  • 6
  • 57
  • 106
Harry
  • 2,177
  • 1
  • 19
  • 33
  • 1
    There's no inheritance in rust. Not like this, anyway. – Sergio Tulentsev Dec 01 '22 at 14:00
  • Does this answer your question: https://stackoverflow.com/a/73163713/5397009 ? – Jmb Dec 01 '22 at 14:02
  • @SergioTulentsev How do I achieve the same functionality in rust. I know I can keep two separate structs namely `CabVehicle` and `PackerMoverVehicle` but I have to repeat lot of fields which I feel isn't good – Harry Dec 01 '22 at 14:03
  • Jmb's answer or this [Is it possible for one struct to extend an existing struct, keeping all the fields?](https://stackoverflow.com/a/32552698/442760) should have what you want. – cafce25 Dec 01 '22 at 14:06
  • @Jmb of course i can create a separate struct which includes common fields and then make a member variable of it in `CabVehicle` struct. But to access fields I have to use an intermediate member variable, which I feel awkward. – Harry Dec 01 '22 at 14:06
  • Rust is not an object-oriented language, so there is no data inheritance mechanism and you have to use composition if you want to avoid repeating fields. – Jmb Dec 01 '22 at 14:13
  • Also relevant: https://stackoverflow.com/q/74637514/5397009 – Jmb Dec 01 '22 at 14:13
  • 4
    It feels awkward becase you are thinking in C++ while writing Rust. If you could explain some particular problem that your C++ design solves, then maybe we could suggest how to solve that in idiomatic Rust. But as-is, your C++ hiearchy does nothing. – rodrigo Dec 01 '22 at 14:16
  • @rodrigo but don't you think it's a good feature. – Harry Dec 01 '22 at 14:24
  • @rodrigo Also if vehicle location needs to be displayed, a member function can be defined only in base class and use it for any of the derived class objects. But in rust I have to implement say "DisplayLocation" trait separately for each vehicle type, since it needs access to fields – Harry Dec 01 '22 at 14:38
  • You can implement the trait for your `struct Shared` and implement `AsRef` for all that share it to avoid implementing the trait in question multiple times. – cafce25 Dec 01 '22 at 14:58
  • I came to Rust from C++, too. I've been there, trying to do inheritance with Rust... and it _can_ be done, sometimes it even feels right. There are plenty of discussions out there about the goods and evils of inheritance in modern programming, such as this [nice one](https://softwareengineering.stackexchange.com/a/260354). – rodrigo Dec 01 '22 at 14:59
  • 2
    Usually aggregation is almost as good as inheritance in reusing code, while being way more flexible. See this simple [playground](https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=c51a63934ab88847f748ac39043ef432). – rodrigo Dec 01 '22 at 15:04

2 Answers2

2

The general answer is to use composition instead of inheritance. Depending on the application, there can be different ways the composition should go. In most cases, you should start with

struct VehicleState {
    lat: f64,
    lon: f64,
    alt: f64,
    speed: f64,
}

The remaining question then is how your different types of vehicles are going to be used.


Way 1: If different parts of the code use the different types of vehicles in distinct, non-overlapping ways, you might simply contain the state struct in the specific structs:

struct Cab {
    state: VehicleState,
    // ... other fields
}

struct PackerMover {
    state: VehicleState,
    // ... other fields
}

This is the version most directly analogous to C++ inheritance, particularly in memory layout and in static typing. However, this makes it awkward to access the common state for different vehicles, and it does not support dynamic dispatch, unless you write a trait with a method to access state (which comes with some limitations in the kinds of code you can write). You should generally avoid this approach unless you know you don't need anything else.


Way 2: If there is code which should be generic over which kind of vehicle is in use, but this is statically decided, you might make a generic struct:

struct Vehicle<T> {
    state: VehicleState,
    details: T,
}

struct Cab { /* ... */ }
struct PackerMover { /* ... */ }

/// This function only works with Cabs
fn foo(vehicle: Vehicle<Cab>) { /* ... */ } 
    
/// This function works with any Vehicle
fn foo<T>(vehicle: Vehicle<T>) { /* ... */ } 

This makes it easy to access the state, and all usage is statically dispatched.

It can be dynamically dispatched too if you make one small change to Vehicle and add a trait:

struct Vehicle<T: ?Sized> { /* ... */
//              ^^^^^^^^ remove default restriction on the type parameter

trait VehicleDetails { /* add methods here */ }
impl VehicleDetails for Cab { /* ... */ }
impl VehicleDetails for PackerMover { /* ... */ }

This allows you to coerce a reference (or pointer or Box too) &Vehicle<Cab> into &Vehicle<dyn VehicleDetails>, which is a type that a pointer to any Vehicle whose T implements VehicleDetails. This can be used to put a variety of vehicles in a Vec<Box<Vehicle<dyn VehicleDetails>>>, for example. Using dyn causes dispatch through vtables, like C++ virtual methods.

(Info on this language feature. The documentation says that “custom DSTs are a largely half-baked feature for now” but this particular case is exactly the case where they do work without any trouble.)

This is not a good choice if you want to be able to find out which “subclass” is being used and interact with it specifically; it is a good choice if all particular characteristics of the vehicle can be expressed within the VehicleDetails trait.


Way 3: If the application is going to be routinely working with dynamically-chosen vehicle types — especially if it frequently wants to ask the question “is this vehicle a Cab” and then interact with its Cabness — then you should probably use an enum to contain the details.

struct Vehicle {
   state: VehicleState,
   kind: VehicleKind,
}

enum VehicleKind {
    Cab {
        seats: u16,
    },
    PackerMover {
        cargo_capacity: u64,
    }
}

This is dynamically dispatched in the sense that every Vehicle can be any kind, so you can always mix-and-match vehicle kinds, but without involving any pointers or vtables. The main disadvantage is that extending it to new kinds requires modifying the single enum VehicleKind, so this is not suitable for a library whose users would be writing subclasses in C++. However, this is a lot less fiddly to work with than the Vehicle<dyn VehicleDetails> I mentioned above.

Kevin Reid
  • 37,492
  • 13
  • 80
  • 108
0

Rust is basically more like a procedural and a functional language with some pseudo-OO features, it’s simultaneously lower-level and more abstract than C++ (closer to C or even to C—, but with ZCA and stronger typing). The answer is: there’s no inheritance in Rust, use composition or just rewrite the whole structure. This may look wild for you, but after some time you will understand that there’s no need in inheritance.

Miiao
  • 751
  • 1
  • 8