Harry
Harry

Reputation: 2961

How to port C++ inheritance to Rust?

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?

Upvotes: 4

Views: 1530

Answers (2)

Kevin Reid
Kevin Reid

Reputation: 43753

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.

Upvotes: 4

Miiao
Miiao

Reputation: 998

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.

Upvotes: 0

Related Questions