Dynamic Dispatch
For a function like
void engageWith(Animal& animal) {
animal.speak();
animal.feed();
}
even though the code says that animal
is an Animal
, we want our program to figure out that the animal
is actually a derived class like Cow
or Raptor
and call the right speak()
member function for that class. In other words, not the static type we saw at compile time, but the dynamic type we actually have for that object at runtime.
Why have we never had to deal with this before?
These issues arise precisely because of subtype polymorphism. We've never had derived classes (or subtypes) before.
The terminology for what we want is dynamic dispatch. “Dispatch” is just “what member function to call” and “dynamic” is because we want it to be determined based on runtime information, not compile-time information.
In some languages, such as Java and Python, dynamic dispatch is the default behavior.
Specifically, languages that say that they are “object oriented” default to this behavior, as that's how most people expect objects to work.
Woah! So C++ isn't object oriented?
In C++, we can get dynamic dispatch if we ask for it. So we can write object-oriented code in C++. Technically, if we don't ask for dynamic dispatch but we do use classes, our code is merely “object based”.
Ugh. Why won't C++ ever just do the right thing without being told??
Zero overhead! Providing dynamic dispatch requires (a little bit of) overhead. And C++ won't make us pay for that feature unless we ask for it.
So how do we ask for it?
Dynamic Dispatch in C++
In C++, a base class can specify exactly which member function(s) will use dynamic dispatch by prefixing that member function with the virtual
keyword, as in
virtual void speak() const;
When a function is marked virtual
in a base class, it's automatically also virtual
in the derived classes. If/when you override that function in a derived class, you should add the keyword override
at the end to explicitly say you are providing a new (overriding) implementation. If you say override
for a member function that wasn't declared virtual
in the base class, you'll get a compiler error alerting you to your mistake.
Why is it
virtual
and notdynamic
? Surelydynamic
would make more sense because it's dynamic dispatch?…
Here's what our Animal
, Cow
, and Raptor
classes look like with dynamic dispatch.
Animal
with Dynamic Dispatch
class Animal {
public:
explicit Animal(std::string name, size_t hunger = 0);
virtual ~Animal() = default;
virtual void speak() const;
virtual void feed();
protected:
std::string name_;
size_t hunger_;
};
Animal::Animal(std::string name, size_t hunger)
: name_{name}, hunger_{hunger} {
// Nothing (else) to do
}
void Animal::speak() const {
// idk what it even means for a generic animal to speak
std::cout << name_ << " makes nonspecific animal noises" << std::endl;
}
void Animal::feed() {
std::cout << name_ << " eats" << std::endl;
if (hunger_ > 0) {
--hunger_;
}
}
Hay! Why are you marking the destructor as
virtual
?The destructor is a pretty important function. If you want to make sure you always call the right function, we certainly want to always call the right destructor. Specifically, it's for when we have an
Animal*
on the heap that we're going todelete
.
Cow
with Dynamic Dispatch
class Cow : public Animal {
public:
explicit Cow(std::string name);
~Cow() override = default;
void speak() const override;
void party() const;
private:
double happiness_;
};
Cow::Cow(std::string name)
: Animal{name, 3}, happiness_{7.5} {
// Nothing (else) to do
}
void Cow::speak() const {
std::cout << name_ << " says: Mooooo" << std::endl;
}
void Cow::party() const {
std::cout << name_ << " parties!!" << std::endl;
}
Shouldn't
party()
be markedvirtual
, too, so that classes that derive fromCow
(RanchCow
, perhaps?) can party in their own special ways?Yes, that would probably have been smart. But by not making
party()
virtual
we're showing that you can still use static dispatch for some functions.
Raptor
with Dynamic Dispatch
class Raptor : public Animal {
public:
explicit Raptor(std::string name);
~Raptor() override = default;
void speak() const override;
private:
double anger_;
};
Raptor::Raptor(std::string name)
: Animal{name, 42}, anger_{11} {
// Nothing (else) to do
}
void Raptor::speak() const {
std::cout << name_ << " says: Rawrrr" << std::endl;
}
Let's Try it Out!
Here's the revised code. Run it and see if it works the way we want now…
Yes!
Mooooo!
Rawrrr!
Gyah! Where did you come from?
Life… uh… finds a way…?
(When logged in, completion status appears here.)