![](https://cdn.theolouvel.com/cat-drawing.webp) ## Abstract In certain situations, monomorphic behavior of derived classes can lead to hardly manageable complexity, making a codebase hard to maintain. For instance, a naïve representation of a zoo could store animals within different containers, depending on the animal type; each type would in turn implement its own behavior. To update this representation, you would need to remember to add a container, a loop for each animal type you introduce to the zoo, along with implementing its specific behavior. Inheritance provides the foundation for polymorphism by enabling us to implement a common interface for different types; but despite that common interface, we're not able to write *polymorphic* code without additional notions. Polymorphism describes the fact that a single piece of code is able to *adapt* to the data types it is applied to. Following [[Occam's Razor|Occam's razor]], a better solution would be to reduce the number of entities and functions we have to maintain and update in our codebase. We can achieve this by storing all animals in a single container, implementing a single loop to iterate over all of them, and define functions that take *any* animal as their parameters. A direct consequence of [[Inheritance|inheritance]] is that an instance of a derived class should be substitutable for an instance of its base class, no matter how far away in the chain, while retaining their specificity. This is known as the **Liskov Substitution Principle**. This will allow us to define the aforementioned components of our system in terms of the base class's type. For this to work, however, we need to tackle two things first: Object slicing, and static dispatch. **Object slicing** is what happens when you store a variable of a derived class inside another variable of one of its base classes. The derived class part gets stripped away, and you lose specific behavior. Object slicing, however, does not occur with pointers and references, because we're passing around a reference to an object instead of the object itself. So writing `Cat c; Animal& a = c;` doesn't slice the `Cat` object. From this we can infer that it is possible to create polymorphic (heterogenous) data containers, where we store objects of various derived class of a base class, loop over them and define functions whose signature has a base class in its parameters and accepts arguments of various derived types. We're left with another problem, which is we can't access the `Cat` methods through an `Animal` reference. This is the case because C++ defaults to **static dispatch**, resolving function calls based on the variable's static type, which here is `Animal`. The compiler looks only at the function present on this type, not those of the *actual* type. This prevents us from calling methods that exist only on the derived class from the base class's interface. Shadowing a method doesn't solve this either: If a method does exist on both the base and derived classes, the base class will still be called. To work around this, we can mark methods as `virtual` in the base class, allowing us to *override* them instead of shadowing them in derived classes. Doing so informs the compiler that it must perform **dynamic dispatch** at runtime by looking at the *actual* type of the object before calling the appropriate overridden method in the derived class. We still won't be able to call methods present only on the derived class from a pointer or reference whose static type is that of the base class, as should be. To achieve that, we would need to check the actual type of the object at runtime using a **dynamic cast**. By combining pointers or references and virtual functions, we can implement polymorphic behavior that's much easier and cleaner to work with. ## The Zoo Problem Suppose you're writing code to represent a zoo. Each animal has its specificities, of course, which can be self-contained in their class. There are also common behaviors between certain animals: both a cat and a tiger are felines, purr, jump, scratch, have claws... Of course, you know about [[Inheritance|inheritance]], so you've defined a class `Feline` to implement those common behaviors, and defined derived classes `Tiger` and `Cat`, encapsulating their own specific traits. Now, the zoo also has its organization, and while some of its processes are animal-dependent, some are applied regardless of species. All animals live in the zoo, so it would make sense to keep them all together. This way we can find them and their information looping over a single data container. If you decide to store them in separate containers, however, one for each and every type of animal you might have, you would have to remember to add a loop in the function that looks for specific animal each time you introduce a new species to the zoo. What's more, you would need to do so for every method of the zoo that can be applied to one of its animals. This is hardly maintainable. At one point or another, you'll make mistakes. Implementing a single data container would enable us to simplify our code by a wide margin; you could then add indefinitely many types of animals to the zoo without having to worry about changing a single method. Let us call such heterogenous containers and functions *polymorphic*. Right now, two things are preventing us from implementing such an abstraction: **object slicing** and **static dispatch**. In the following sections, we'll see how using **pointers and references** solves object slicing, and how **virtual functions** solve the problem of static dispatch, allowing for true polymorphic behavior. ## Object Slicing Object slicing is what happens when you assign an object to a variable of one of its parents' type (the only situation where that could work). The assigned object loses any additional members he might have had compared to its parent. For now, there are only felines in the zoo, so we'll start with that. Below is an illustration of its process. A `Cat` is fit into a `Feline`. By this process, the cat is stripped of everything that makes it a `Cat`, while retaining all that makes him a `Feline`. ```cpp class Feline { public: void purr() { std::cout << "Purrrrr..." << std::endl; } }; class Cat : public Feline { public: void purr() { std::cout << "Meeeoooooow..." << std::endl; } void jump() { std::cout << "Jump !" << std::endl; } }; int main() { Feline f; Cat c; /* Object slicing happens here We strip the c of what makes it a Cat so that it's left with what makes it a Feline */ f = c; /* ✅ Outputs "Purrrrr..." to the console f is of type Feline, and feline have a member purr() The cat's 'purr' member is, however, ignored */ f.purr(); /* 🚫 The following yields a compile time error The (static) type of f is Feline, which has no member jump(). */ f.jump(); return 0; } ``` From this, we can infer that storing heterogenous types into an `std::vector` will, in fact, not store the objects as we intend, but sliced version of them. ```cpp int main() { std::vector<Feline> zoo; Cat c; Tiger t; zoo.insert(zoo.end(), { c, t }); return 0; } ``` Indeed, if we run the code above, the vector `zoo` now only has two indistinguishable `Feline`objects. ## Pointers & References Prevent Slicing We can prevent object slicing using either references or pointers. >👉 A good rule of thumb: Use references where you can, and pointers where you must. Coming back to our previous example, let's try to make the cat jump. ```cpp int main() { Cat c; // USING A REFERENCE Feline& f1 = c; /* 🚫 Compile-time error: static type is Feline& A different error ! This is progress. f1's static type is Feline&; it has no members jump() */ f1.jump(); // USING A POINTER Feline* f2 = &c; // address-of operator /* 🚫 Compile-time error: static type is Feline* f2's static type is Feline*; it too has no members jump() */ f2->jump(); // -> calls a member method through a pointer return 0; } ``` This still doesn't work ! However, now we see a different error, demonstrating the `Cat` identity wasn't sliced away. So at least now we know how to prevent object slicing. ## Polymorphic Data Containers From this we can infer that preventing object slicing in a `vector` must be possible, too. It is. There's, however, one distinction: You cannot create an `std::vector`, or any other standard container, of references. Think of references as nicknames. A nickname is not the person it refers too, even if everybody understands who you're talking about. This is the case because references themselves are not objects, but aliases for other objects that must be initialized to refer to an object when they're created. They cannot be empty or reassigned to refer to something else. You can probably see this is a recursive problem: a vector of references would need to hold references to the objects they refer to somewhere else. Also, an `std::vector` needs to be able to move its elements around in memory, which require the ability to reassign them, something references can't do. Since references don't make any sense for our use case, we're left with no choice but to use pointers then. That way, our objects will not be truncated, thus retaining their specificities. ```cpp class Feline { /* ... */ }; class Tiger : public Feline { /* ... */ }; class Cat : public Feline { /* ... */ }; class Zoo { public: void introduce(std::initializer_list<Feline*> felines) { zoo.insert(zoo.end(), felines); } private: std::vector<Feline*> zoo; }; int main() { Zoo z; /* ✅ Successfully inserts both a Cat and a Tiger into the zoo ! They have retained their properties. */ z.introduce({ new Cat(), new Tiger() }); return 0; } ``` We successfully created an **heterogenous data container**. However, there's now a bug in our program. Because we're using C-like pointers and dynamically allocate memory using the keyword `new`, we are now responsible for deallocating it as well. Failing to do so creates memory leaks. This must happen when `zoo` goes out of scope and gets destroyed. ```cpp class Zoo { public: ~Zoo() { for (Feline* feline : zoo) { delete feline; } } void introduce(std::initializer_list<Feline*> felines) { zoo.insert(zoo.end(), felines); } private: std::vector<Feline*> zoo; }; ``` A better way would be not to have to worry about managing memory at all. This we can achieve by leveraging [[Pointers & References|smart pointers]] made available in recent editions of C++ to address this recurring issue. Going back to the `zoo`, we notice something else. What's true for variables and containers is also true of function parameters. The function `void introduce(std::initializer_list<Feline*>)` accepts a list of `Feline` objects, without slicing them either. This means we can write a single function for heterogenous data types. We're getting closer ! >⚠️ There's one thing to be wary of. If you forget to make function parameters references in the signature, you're passing objects by value. If you pass objects by value, you're calling the copy constructor. And because the parameter's static type will be `Feline`, its constructor will be called, slicing the object. We haven't defined what's a static type yet, but that's the next part. Had our function's signature been `void introduce(std::initializer_list<Feline>)`, every `Cat` and `Tiger` would have been sliced down to a `Feline` before even entering the function. This is why passing polymorphic objects by reference or pointer is so critical. ## Static Dispatch Calls Explicit Type Method We're still missing something. While our object hasn't been sliced and, and a `Cat` retains everything that makes it a cat when it enters the zoo, we still seem to be unable to access its members. This is due to static dispatch. C++ default *modus operandi* is to lookup an object's members based on its explicit type. Because the explicit type of our zoo's inhabitants is `Feline`, when you try to call `void Cat::jump();`, the compiler is actually looking for `Feline::jump();`. Because it doesn't exist, we run into a compile-time error. The same would have happened if we had shadowed `Feline`'s methods in the `Cat` class, implementing a different behavior. Instead of running into an error, the `Feline`'s member function would have been called, which is still not what we want. C++ offers a way to circumvent this, by opting out of the default behavior and asking the compiler to replace a static dispatch with a dynamic one, so we can access non-sliced yet hidden members of the zoo's inhabitants. ## Derived Classes Shadow Base Class Members Right now, the method called is dependent on the static type of the object. If the static type is that of the base class, then the base class's method is called. If the static type is that of the derived class, say `Cat`, then the derived class's method is called. The latter happens because until now, when we define a method in a derived class that is also present in the base class, we are merely [[Inheritance#Shadowing|shadowing]] it and hiding the base class's member. This does not not *override* the base class's method; it just makes it difficult to reach. Let's consider the somewhat more abstract following example. ```cpp class A { public: void test(const std::string& str) const { std::cout << 'A-str' << std::endl; } void test(int i) const { std::cout << 'A-int' << std::endl; } }; class B : public A { public: void test(int i) const { std::cout << 'B-int' << std::endl; } }; int main() { B b; b.test(2); /* 🚫 Compile-time error Shadowing hides the member methods of the base class no matter their signature */ b.test("test"); // ✅ You need to you scope resolution operator to access it b.A::test("test"); b.A::test(2); // member methods of A continue to exist through shadowing B b; A* pa(nullptr); pa = &b; // ✅ Because we're working with an A* pointer, we're in A:: scope // static dispatch, but those are pointers + virtual??? // contradicts the example above // Makes no fucking sense pa->test("2"); pa->test(2); return 0; } ``` Leveraging the scope resolution operator `::`, we are still able to access the base class's members that have been shadowed. Shadowing doesn't override members, it just makes them harder to reach. You see that because we shadowed the `void test(int i)` method of class `A` in class `B`, *none* of class's `A` member can be directly called anymore without using the scope resolution operator. Shadowing occurs only with consideration for the name of the function, and none for its signature. The modern C++ standard is to access the base class's member is with the help of a `using` declaration that allow us to bring the hidden base class names into the derived class's scope. ```cpp class B : public A { public: // Unhide all of A's member methods using A::test; // Shadow just the one we want void test(int i) const { std::cout << 'B-int' << std::endl; } }; ``` >👉 You can use `using` declarations not only to unhide then shadow, but also *override* members, which is our next topic. What we want instead is to *replace* the base class's member with specialized behavior, so that the derived class's definition is consistently called, regardless of its static type. This we can achieve by allowing the base class's function to be overridden. ## Virtual Members Get Dynamically Dispatched The *type* of the variable or parameter dictates what function will be called. How do we work our way around static dispatch and call the member methods base on the actual type of the object then? We enable **dynamic dispatch** on member methods of the base class by prefixing them with the `virtual` keyword. ```cpp class Feline { public: virtual void purr() { std::cout << "Purrrrr..." << std::endl; } }; class Cat : public Feline { public: void purr() { std::cout << "Meeeoooooow..." << std::endl; } void jump() { std::cout << "Jump !" << std::endl; } }; int main() { Cat c; Feline& f = c; /* ✅ Outputs "Meeeoooooow..." ! At last, we're able to call member methods. */ f.purr(); return 0; } ``` This time we're successfully able to call the member methods as defined in the derived class, because we opted in for dynamic dispatch with the use of the `virtual` keyword. However, it is not possible to call `void Cat::jump()` through the base-class `Feline` interface. Polymorphism is designed to let us work with any derived `Feline` object through this common interface, and **dynamic dispatch** allows us **override** member methods of the `Feline` class by making them `virtual` to create specialized behavior. Here, we simply added a new member method on the `Cat` class, which exist outside the common contract and can never be called on an instance whose static type is `Feline`. This only way to call `void jump()` on such an object is to use a **dynamic cast** to check the object's real type at runtime. This results in the implementation of monomorphic (type-specific) behavior within polymorphic functions, which, while doable, is often not ideal as it breaks the abstraction that makes polymorphism so helpful. Member methods marked as `virtual` in base classes stay virtual in derived classes by **transitivity**, even if we don't explicitly mark them as such in the derived class. ### Explicit Overrides Looking back at our program, we'd probably like for all `Feline` derived objects to be able to jump. So the real problem isn't so much that we can't call `void Cat::jump()`, but that we forgot to define it in our base class. The language offers some guardrails to prevent such situations from happening in the first place. A good practice of modern C++ is to mark methods that are intended to be overrides of a base class's members must be marked with `override`. ```cpp class Cat : public Feline { public: void purr() override { std::cout << "Meeeoooooow..." << std::endl; } void jump() override { std::cout << "Jump !" << std::endl; } }; ``` By doing so, we tell the compiler that this specific member is supposed to be an `override` on a `virtual` method of the base class. If the compiler doesn't find the `virtual Feline::jump()` method in the base class, it yields an error, pushing us to fix the real error in the base class. Now that our code doesn't compile we see the error. ```cpp class Feline { public: virtual void purr() { std::cout << "Purrrrr..." << std::endl; } virtual void jump() { std::cout << "Jump !" << std::endl; } }; ``` Using **explicit overrides** prevents us from accidentally shadowing or defining new methods when we wanted to override an existing one instead. This matters because it ensures you get the polymorphic behavior you want. Using `override` guarantees a dynamic dispatch call, whereas accidentally shadowing or defining a new method would result in static dispatch, leading to subtle runtime bugs. We've made this example fairly obvious, but imagine having a typo in the member method's name, whether it is in the base or derived class. You probably would want to catch that at compile-time instead of having your program crash in the middle of execution. ### Final Member Methods Another guardrails modern editions of C++ offer is the `final` keyword, which prevents derived classes from overriding the base class's member method. Since only `virtual` functions can be overridden, `final` is only meaningful when applied to a `virtual` function. ```cpp class A { public: virtual void hello(); virtual void goodbye() final; }; class B : public A { public: virtual void goodbye() override; // 🚫 Error: Cannot override a final function virtual void goodbye() override; }; ``` >👉 Marking a function both as `virtual` and `final` provides an **optimization opportunity** for the compiler. Whenever it sees a call to `virtual final` method through a pointer or reference whose static type is that of the class that declared it final, the compiler can perform **devirtualization**, replacing the dynamic dispatch with a static function call. This means that you'll often see `final` not as a safeguard against overriding, but as a **performance hint** to the compiler. Lastly, the `final` keyword can be used to prevent the creation of derived classes. ```cpp class Sterile final {} class C : public Sterile // 🚫 Compile-time error {} ``` ### Virtual Calls Are Statically Dispatched In Constructors Constructors cannot be made virtual; a virtual call is resolved at runtime based on the object's actual type, but how can it have an actual type if it doesn't exist yet? Behind every polymorphic object is a hidden pointer called `vptr` that points to a table of function pointers for that specific class. To make a virtual call, the program follows the object's `vptr` to find the correct `vtable` and call the appropriate function. A virtual constructor would need the `vtable` to be set up before it's called, when it's the constructor's responsibility to setup the `vtable`. So it is not possible to determine the actual type of an object *before* its constructor ran. It's a paradox, much like putting two mirrors in front of each other and asking which one reflected the other first. From the compiler's perspective, this would mean not telling it what the type of an object is and ask it to guess. ```cpp class A { public: A() { f(); } virtual void f() const { std::cout << "A::f()" << std::endl; } }; class B : public A { public: virtual void f() const { std::cout << "B::f()" << std::endl; } }; int main() { A a; // >> "A::f()" B b; // >> "A::f()" A* pa = &b; pa->f(); // >> "B::f()" return 0; } ``` Constructors cannot be made `virtual`. Following the same logic, calling a virtual function from a constructor does not result in dynamic dispatch. The method called will be that of the base class. Indeed, when we initialize `b`, the first constructor to be called is that of the base class `A`, which means the virtual override doesn't even exist at this point in time. It's only after `b` is fully constructed that its `vptr` points to `B`'s `vtable`, allowing `pa->f()` to be dynamically dispatched as intended. >👉 A way exists, however, to create a copy of an object without knowing its specific derived type, called the **virtual constructor idiom**, relying on the implementation of a `virtual clone()` method in the base class. That's beyond the scope of this article. ### Virtual Destructors Prevent Memory Leaks >👉 If a class has even just one virtual function, it needs a virtual destructor. Let's take a look at how derived classes are instantiated, and what the order of operations looks like. ```cpp class A { public: A() { std::cout << "Initializing A..." << std::endl; } virtual ~A() { std::cout << "Destructing A..." << std::endl; } virtual void test() { std::cout << "Testing A..." << std::endl; } }; class B : public A { public: B() { std::cout << "Initializing B..." << std::endl; } virtual ~B() { std::cout << "Destructing B..." << std::endl; } virtual void test() { std::cout << "Testing B..." << std::endl; } }; int main() { if (true) { B b; // Initializing A... // Initializing B... } // => 'b' goes out of scope // Destructing B... // Destructing A... return 0; } ``` As you can see, the constructor of a derived class is invoked after the default constructor of the base class has been called (unless another constructor has been called explicitly). Upon destructing the object, operations occur in the reverse order; the destructor of the derived class is called before that of the base class. As seen in the previous section, constructors cannot be made virtual. Indeed, their job is to set the object's hidden virtual pointer `vptr` to point to the correct `vtable` for its class, which is precisely the mechanism that enables dynamic behavior. The exact opposite must then be true of destructors; where there are virtual members, the destructor *must* be made virtual. Otherwise destructors would be called based on the static type of the object, which is problematic for the implementation of polymorphic behavior. Take for instance the `Zoo` example. It holds on to pointers to various types of animals with `std::vector<Feline*> zoo;`. We must therefore implement a destructor that will make sure that animals behind those pointers are appropriately destructed, too, once an instance of `Zoo` goes out of scope. ```cpp class Zoo { public: ~Zoo() { for (Feline* feline : zoo) { delete feline; } } // ... private: std::vector<Feline*> zoo; }; ``` If `Feline`'s destructor is not `virtual`, then only its destructor will be called upon freeing the felines. This means that the `Cat` and `Tiger` parts will never be free, creating a memory leak. To make sure that each `Feline`'s derived class is also called before that of the base class, we mark its destructor as `virtual`. ```cpp class Feline { public: virtual ~Feline() = default; // ... }; ``` By marking the destructor as virtual, we make sure the destructor is called *dynamically*, based on the real type of the object, thus freeing all memory in a cascade-like fashion; first calling the destructor of the real type, then parent after parent until we're done working our way up the inheritance chain. ## Abstract Classes Until now we've defined virtual methods within the base class and overridden them in derived classes. This allows us to implement specialized behaviors *sometimes*. This is a solid pattern when you need the base class to provide default implementations. However, in some cases you might want to enforce a structure *all the time*, because it doesn't make sense to have a generic implementation. Consider an `Animal` class that we might want to define some day, once our zoo is ready to accept more than just felines. You would want all animals to have a functions that allows them to move or make sounds. But wait, how does an abstract animal moves and what does it sound like? For this precise reason, C++ offers the ability to create *abstract* classes, which do not provide default implementations of the methods they declare. This way we can enforce a structure, making a contract with the compiler that derived classes will have to provide their own implementations of such abstract methods. Classes are said to be abstract if they have at least one *pure* virtual function. Not all member methods need to be. A pure virtual function is a virtual function definition where the body of the function is replaced with `= 0;` ```cpp class Animal { public: Animal(): id_(++last_given_id) {} virtual void move() const = 0; virtual void make_sound() const = 0; virtual int get_id() const { return id_; } private: static int last_given_id; int id_; }; int Animal::last_given_id = 0; ``` Abstract classes act as interfaces, enforcing a contract that all concrete derived classes must follow by implementing the pure virtual functions it defines. >👉 Since abstract classes don't provide actual implementation of member methods, if follows you cannot create instances of abstract classes; they can only be inherited. ## Polymorphic Behavior Now that we've seen how we can store heterogenous types derived from a common base class in a single collection, and that functions can have polymorphic signatures, we can tie it all together to implement polymorphic behavior. One last thing you would want to do is to leverage [[Pointers & References|smart pointers]] introduced in recent editions of C++. This way you would not have to worry about freeing memory, thus removing the need for `Zoo`'s destructor. ```cpp class Feline { public: virtual ~Feline() = default; virtual void purr() = 0; virtual void jump() = 0; }; class Cat : public Feline { public: virtual void purr() final { std::cout << "The cat purrs..." << std::endl; } virtual void jump() final { std::cout << "The cat jumps !" << std::endl; } }; class Tiger : public Feline { public: virtual void purr() final { std::cout << "The tiger purrs..." << std::endl; } virtual void jump() final { std::cout << "The tiger jumps over the cat !" << std::endl; } }; bool coin_flip() { /* ... */ } class Zoo { public: void introduce(std::unique_ptr<Feline> new_feline) { zoo.push_back(std::move(new_feline)); } void animate() { while (true) { for (const auto& feline_ptr : zoo) { if (coin_flip()) { feline_ptr->jump(); } else { feline_ptr->purr(); } } } } private: std::vector<std::unique_ptr<Feline>> zoo; }; int main() { Zoo z; z.introduce(std::make_unique<Tiger>()); z.introduce(std::make_unique<Cat>()); z.animate(); return 0; } ``` >👉 The safe, idiomatic way for a function to *sink* or *take ownership of* an object is to accept a `std::unique_ptr` by value, then using `std::move` to transfer ownership to the function. ## Performance Considerations Polymorphism comes at the cost of a small overhead, often negligible when weighted against its benefits. This overhead comes not from a type check, but from a couple of extra memory lookups to follow the type's `vptr` to the correct `vtable` where it can find the address of the function that needs to be called. However, there's some optimization margin for the compiler. While it's true you can *only* get dynamic resolution through a pointer or reference and using virtual methods, you don't *always* get dynamic resolution under those circumstances, as the compiler is able to transform some virtual calls into static dispatches where it's able to determine the object's type ahead of runtime. This process is called devirtualization.