Pure Virtual Member Functions
I was saying I didn't see the point of being able to make vanilla
Animal
objects—we only want to make objects of classes derived fromAnimal
.And, now that you mention it, the
Animal::speak()
function was kinda creepy, with its “nonspecific animal noises”. Can that go away?You both get your wish.
In C++, it's possible to declare a function as a pure virtual function, which means that we declare it in the base class, but say that the base class doesn't implement that function. All (concrete) derived classes must override it and provide their own implementation.
When we use this approach, it also makes the base class an abstract class, which means that we can't make any objects of that base class, only objects that derive from it.
Declaring a Pure Virtual Function
We use the syntax
virtual void speak() const = 0;
The = 0;
means “make this function pure virtual”.
Okay, I'd managed to swallow saying things like
= delete;
to not have a function at all, but= 0;
? Really? Couldn't we have had= abstract;
or something?When I was first adding this feature, I wasn't sure if people would be up in arms if I added a new keyword to the language. But I figured I could get away with
= 0;
. And it reminded me of how—internally—the system stores a null pointer in the virtual-function table that is used to implement dynamic dispatch.What's a virtual-function table?
Each class has a vtable—a table of pointers to each of its virtual functions, and each object points to its class's vtable. That's how dynamic dispatch works behind the scenes: it finds the vtable, goes to a specific offset to find the function it needs, and calls that function.
But the C++ standard doesn't even acknowledge that dynamic dispatch is implemented using vtables! You can't really expect other people to be thinking about those arcane C++ implementation details, can you?
…
Abstract Classes as Interfaces
In Java, they also have interfaces. Does C++ have that?
Basically, yes.
If we make a base class that has
- No data members, and
- All member functions declared as pure virtual,
that class is a special kind of abstract base class: an interface class. It specifies what the class needs to be able to do, without saying anything about how.
Let's look at an example:
class Merrymaker {
public:
virtual ~Merrymaker() = default;
virtual void party() const = 0;
};
We can adjust our Cow
class to say it is also a Merrymaker
as follows:
class Cow : public Animal, public Merrymaker {
public:
explicit Cow(std::string name);
~Cow() override = default;
void speak() const override;
void party() const override;
private:
double happiness_;
};
So you can inherit from multiple classes?
Yes! As many as you like!
But in modern C++, if you inherit from multiple classes, the extra ones should all be interface classes. Experience has shown that people get pretty confused otherwise.
But that's just a programming practice. You can do as you please.
Let's also make a class that isn't an Animal
class but does support the Merrymaker
interface:
class PrimeMinister : public Merrymaker {
public:
explicit PrimeMinister(std::string name);
void party() const override;
private:
std::string name_;
double charisma_ = 1.0;
};
PrimeMinister::PrimeMinister(std::string name) : name_{name} {
// Nothing (else) to do
}
void PrimeMinister::party() const {
std::cout << name_ << " parties!! (but claims it's all work)"
<< std::endl;
}
And here's some code that uses our Merrymaker
interface:
std::vector<Merrymaker*> partyGoers;
partyGoers.push_back(new Cow{"Bessie"});
partyGoers.push_back(new PrimeMinister{"Boris Johnson"});
partyGoers.push_back(new PrimeMinister{"Justin Trudeau"});
partyGoers.push_back(new Cow{"Mabel"});
for (Merrymaker* partyGoer : partyGoers) {
partyGoer->party();
}
while (!partyGoers.empty()) {
delete partyGoers.back();
partyGoers.pop_back();
}
Try out this code:
I noticed that the code for
engageWith()
had an extra bit to see if it was really aCow
usingdynamic_cast
.That's right. These are similar to the casts you often have to do when writing object-oriented code in Java, to try to turn an object that is only known to be a base class (like
Animal*
) into a specific derived class (likeCow*
).If the object isn't the one you hoped for, the cast fails (returning a null pointer for pointers, or throwing
std::bad_cast
for references). You can only usedynamic_cast
on a object that has at least one virtual function. Casting to aCow
is known as a downcast, but you could change the code to try to cast to aMerrymaker
instead—that would be a sidecast.We really don't need to dive down the
dynamic_cast
rabbit hole!Will I ever actually use any of this stuff?
These ideas are in a lot of languages, including Java. In C++, we're just more explicit about what's going on.
If you program in Swift, they call interfaces protocols, and if you code in Haskell, they're called type classes. The details vary, but the core ideas are fundamentally the same.
So, I guess we could say that C++ has all the object-oriented stuff you'd ever want. But if you don't want or need to use it, that's a valid choice, too. Do whatever feels simplest?
That'll do it!
(When logged in, completion status appears here.)