Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
the classic Animal class example in C++ and Rust
// the OOP version in C++
#include <iostream>
// base abstract class. that is what we use as the interface
class Animal
{
public:
Animal(char const* name): m_name(name) {}
// this is required to properly delete virtual classes
virtual ~Animal() {}
// with this strange syntax we define an unimplemeted "interface" function
virtual void make_sound() = 0;
protected:
// shared data field
std::string m_name;
};
// derived class. that means that Dog is a more refined version of Animal
// you usually inherit from only a single base class
class Dog: public Animal
{
public:
// need to forward the constructor arguments
Dog(char const* name): Animal(name) {}
// here we implement the interface
void make_sound() override
{
std::cout << m_name << " the dog said: bork!" << std::endl;
}
// type-specific method
void wag()
{
std::cout << "*" << m_name << " wags*" << std::endl;
}
};
// same as above, but with a different implementation
class Cat: public Animal
{
public:
Cat(char const* name): Animal(name) {}
void make_sound() override
{
std::cout << m_name << " the cat said: mow!" << std::endl;
}
void purr()
{
std::cout << "*" << m_name << " purrs*" << std::endl;
}
};
// We can now use Animal as the general type
// There is runtime type information that allows this (dynamic dispatch)
void animal_sound(Animal& a)
{
// method called via the generic interface
a.make_sound();
// type information is still there, we can extract the original type by
// probing if dynamic cast works
auto d = dynamic_cast<Dog*>(&a);
if (d != nullptr)
d->wag();
auto c = dynamic_cast<Cat*>(&a);
if (c != nullptr)
c->purr();
}
// we can do static dispatch too, via templates (duck typing)
template <typename T>
void animal_sound_1(T& a)
{
a.make_sound();
// however, we can't easily access type information here without
// using specialization..
}
// ..and that's pretty much equivalent to overloading the function
// this works by making two different functions with internally mangled names
// for each argument combination
void animal_sound_2(Dog& a) { a.make_sound(); a.wag(); }
void animal_sound_2(Cat& a) { a.make_sound(); a.purr(); }
int main()
{
auto cat = Cat("kitty");
auto dog = Dog("puppy");
animal_sound(cat);
animal_sound(dog);
animal_sound_1(cat);
animal_sound_1(dog);
animal_sound_2(cat);
animal_sound_2(dog);
return 0;
}
// how we implement this on Rust..
// instead of a base class, we define a "trait"
// traits define a functionality a type must provide. they're rougly like interfaces
// unlike classes, traits don't contain any data
trait Animal
{
fn make_sound(&self);
}
// structs contain only the declared fields, no inheritance
// (derive can automagically implement some special traits)
#[derive(Clone)]
struct Dog
{
name: String,
}
// methods are not part of the struct, they can be appended at any time
impl Dog
{
// rust types have only one constructor, the default one (used below)
// but it's common to create a "new" function for that purpose
fn new(name: &str) -> Self
{
// to_owned copies the borrowed string (&str) into a owned String
Dog{ name: name.to_owned() }
}
fn wag(&self)
{
println!("*{} wags*", self.name);
}
}
// now, we say that "Dog is an Animal" by implementing the corresponding trait
// there is no hierarchy, and a type can implement multiple traits
impl Animal for Dog
{
fn make_sound(&self)
{
println!("{} the dog said: bork!", self.name);
}
}
// now the same, but with Cat
#[derive(Clone)]
struct Cat
{
name: String,
}
impl Cat
{
fn new(name: &str) -> Self
{
Cat{ name: name.to_owned() }
}
fn purr(&self)
{
println!("*{} purrs*", self.name);
}
}
impl Animal for Cat
{
fn make_sound(&self)
{
println!("{} the cat said: mow!", self.name);
}
}
// we can use Animal as a "trait object", that means dynamic dispatch
// there is only 1 copy of this function
// but this isn't the best method..
fn animal_sound(a: &Animal)
{
a.make_sound();
// however type information is erased ..
}
// static dispatch is done via generics. it's similar to templates but it requires
// a "trait bound", that means only Animal methods are accessible from here.
// the compiler creates two copies of this function, one for each type
// (with `T: Animal + ?Sized` it allows both static and dynamic dispatch from the same function)
fn animal_sound_1<T: Animal>(a: &T)
{
a.make_sound();
// no type information here too ..
}
// .. to preserve type information, we use a enum (sum type, also called ADT)
enum AnyAnimal
{
Dog(Dog),
Cat(Cat),
}
// since it's the sum of two animals, it should be an Animal too
impl Animal for AnyAnimal
{
fn make_sound(&self)
{
// to use an enum, we extract it's data via a "match" statement
match *self
{
AnyAnimal::Dog(ref d) => d.make_sound(),
AnyAnimal::Cat(ref c) => c.make_sound(),
}
}
}
// now we got an heterogeneous type with full type information
fn animal_sound_2(a: AnyAnimal)
{
a.make_sound();
match a
{
AnyAnimal::Dog(d) => d.wag(),
AnyAnimal::Cat(c) => c.purr(),
}
}
// we can make things more transparent to the user by implementing the From trait
impl From<Dog> for AnyAnimal
{
fn from(dog: Dog) -> Self
{
AnyAnimal::Dog(dog)
}
}
impl From<Cat> for AnyAnimal
{
fn from(cat: Cat) -> Self
{
AnyAnimal::Cat(cat)
}
}
// "Into" is the counterpart of From
fn animal_sound_3<T: Into<AnyAnimal>>(anim: T)
{
let a = anim.into(); // run the conversion and extract the AnyAnimal
a.make_sound();
match a
{
AnyAnimal::Dog(d) => d.wag(),
AnyAnimal::Cat(c) => c.purr(),
}
}
fn main()
{
let cat = Cat::new("kitty");
let dog = Dog::new("puppy");
animal_sound(&cat);
animal_sound(&dog);
animal_sound_1(&cat);
animal_sound_1(&dog);
// using the enum directly is verbose
animal_sound_2(AnyAnimal::Cat(cat.clone()));
animal_sound_2(AnyAnimal::Dog(dog.clone()));
// using Into arguments makes it prettier
animal_sound_3(cat);
animal_sound_3(dog);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.