Created
August 10, 2016 04:42
-
-
Save wolfiestyle/eeb7e48a45f1b78db4a2c6ebfd01e926 to your computer and use it in GitHub Desktop.
the classic Animal class example in C++ and Rust
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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); | |
} |
On my Mac (rustc 1.72.0 (5680fa18f 2023-08-23) (Homebrew))
This does not compile - but I get the error
error[E0782]: trait objects must include the `dyn` keyword
--> src/main.rs:77:20
|
77 | fn animal_sound(a:&Animal)
| ^^^^^^
|
help: add `dyn` keyword before this trait
|
77 | fn animal_sound(a:&dyn Animal)
| +++
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
It's a good example about encapsulate, inherit, polymorphism and generic programming. It helps a lot.