Skip to main content

Someone wanted to learn this too, so Grasp built them a personal learning path.

You can keep exploring this one or create your own

Polymorphism: Inheritance and Virtual Functions

In our previous lesson, we established the fundamentals of C++ classes, understanding them as blueprints for creating objects that bundle data and behavior. Today, we'll build directly on that foundation to explore two of the most powerful concepts in object-oriented programming: inheritance and polymorphism. These principles are essential for organizing complex software like rendering engines, where you might have a general SceneObject class with more specialized derived classes like Camera, Light, and Mesh. This lesson will equip you to create such hierarchies, enabling you to write more flexible, reusable, and manageable code for your future graphics projects.

By the end of this lesson, you will be able to implement inheritance to create relationships between classes and use virtual functions to achieve polymorphic behavior, where different objects can be treated uniformly through a common interface.

Inheritance: Building Class Hierarchies

Inheritance is a mechanism that allows you to create a new class (a derived class or subclass) from an existing class (a base class or superclass). The derived class inherits the members (data and functions) of the base class, allowing you to reuse common code and establish an "is-a" relationship. For example, a Player "is an" Entity.

The primary benefit of inheritance is avoiding code duplication. Instead of copying and pasting common functionality across multiple classes, you can place it in a single base class.

To see a practical demonstration of this, let's watch a video that explains the motivation and basic syntax.

Inheritance in C++

This video from The Cherno's C++ series clearly illustrates the problem of code duplication and how inheritance solves it.

Watch from the beginning to understand the "why" of inheritance. Pay close attention to how the Player class is initially written with duplicated code from the Entity class (code duplication) and then refactored to inherit from Entity using the class Player : public Entity syntax (inheritance syntax).

As you saw, the syntax is straightforward. class Derived : public Base establishes that Derived inherits publicly from Base. This means public members of Base remain public in Derived.

The protected Access Specifier

In the last lesson, we covered public and private. Inheritance introduces a third access specifier: protected.

  • public: Accessible from anywhere.
  • private: Accessible only by member functions of the class itself. Derived classes cannot access private members of their base class.
  • protected: Accessible by member functions of the class itself and by member functions of any derived classes.

When designing a base class, you'll often make member variables protected so that derived classes can directly interact with them, while still preventing access from outside the class hierarchy.

Lesson 18 - Inheritance and Polymorphism I

This article provides a concise explanation of protected and the syntax of inheritance.

Please read the section on "Access Modifiers," which defines the protected keyword (the definition). Then, review the "Using Inheritance" section, focusing on the declaration syntax and how different types of inheritance (public, protected, private) affect member access (the syntax). For our purposes, we will almost always use public inheritance.

Polymorphism: One Interface, Many Forms

Inheritance lets us share code, but its true power is unlocked with polymorphism. The term means "many forms," and in OOP, it refers to the ability to treat objects of different derived types as if they were objects of their base type. For example, you could have a list of SceneObject pointers, where some point to Camera objects, some to Light objects, and some to Mesh objects. You could then iterate through this list and call a draw() function on each one, and the correct draw() implementation for each specific object type would be executed.

This is achieved through virtual functions. However, without them, C++'s behavior might surprise you. If you have a base class pointer that points to a derived class object and you call a method, the compiler will, by default, call the base class's version of that method. This is called static binding or early binding, because the function call is resolved at compile time based on the pointer's type, not the object's actual type.

Let's see this problem in action.

Virtual Functions in C++

The Cherno again provides a perfect example of the problem that virtual functions solve.

Watch the first part of this video (the problem). The code creates a base Entity class and a derived Player class, both with a GetName() method. Notice what happens when a Player object is passed to a function that expects an Entity*: the Entity's version of GetName() is called, which is not what we want.

The Solution: virtual and override

To get the behavior we want—where the derived class's method is called—we need to tell the compiler to use dynamic binding or late binding. This means the function call is resolved at runtime, based on the actual type of the object being pointed to.

We enable this by declaring the function in the base class with the virtual keyword.

This diagram illustrates the core concept: because `print()` is declared `virtual` in the `Base` class, calling it through a `Base*` that points to a `Derived` object correctly invokes the `Derived` class's implementation.

When a derived class provides its own implementation for a virtual function from its base class, it is called function overriding. While not strictly required, it is modern C++ best practice to mark the overriding function in the derived class with the override keyword. This has two benefits:

  1. It makes the code more readable by explicitly stating your intent to override a base class function.
  2. It allows the compiler to catch errors. If you misspell the function name or change its parameters, the compiler will give you an error because you're not actually overriding anything.

Virtual Functions in C++

This video continues by showing the simple fix and introducing the override keyword.

Now watch the next segment. See how adding the virtual keyword to the base function solves the problem (the virtual keyword). Then, pay attention to the explanation of the override keyword and its benefits (the override keyword).

How Virtual Functions Work: The V-Table

Since you're interested in how things work at a lower level, let's briefly discuss the mechanism behind virtual functions. When a class has at least one virtual function, the compiler creates a virtual table, or vtable, for that class. The vtable is a static array of function pointers, with one entry for each virtual function in the class.

Furthermore, the compiler adds a hidden pointer to each object of that class, often called the virtual pointer or vptr. This vptr points to the vtable for the object's class.

When you make a virtual function call through a base class pointer, the program follows these steps at runtime:

  1. Use the object's vptr to find its vtable.
  2. Look up the correct function pointer in the vtable using a fixed index for that function.
  3. Dereference the function pointer to call the function.

This lookup is what enables dynamic dispatch. A Player object's vptr points to the Player vtable, while an Entity object's vptr points to the Entity vtable.

This diagram shows a `parent` class and two `child` classes. Each class has its own vtable containing pointers to its implementations of the virtual functions. Each object instance contains a `_vptr` that points to the appropriate vtable, enabling the correct function call at runtime.

This mechanism has a small performance cost: the extra memory for the vptr in each object and the extra indirection of the vtable lookup for each call. However, as noted in the video, this cost is usually negligible in modern applications and is a small price for the immense flexibility polymorphism provides.

Critical Considerations: Virtual Destructors and Abstract Classes

Two final concepts are crucial for using polymorphism correctly and effectively.

1. Virtual Destructors

Rule of thumb: If your class has any virtual functions, you should immediately give it a virtual destructor.

Consider what happens when you delete a derived object through a base class pointer:

  • If the base class destructor is not virtual, only the base class destructor is called. The derived class destructor is never run. This will lead to resource leaks (e.g., memory allocated by the derived class will not be freed).
  • If the base class destructor is virtual, the derived class destructor is called first, followed by the base class destructor. This ensures proper cleanup.

Polymorphism in C++ Programming - Startertutorials

This article gives an excellent, clear example of why virtual destructors are necessary.

Scroll to the section "Virtual Constructors and Destructors." First, read the explanation and look at the code example that shows the problem: the memory leak. Note that only A's destructor is called. Then, look at the very next code block, which shows how adding virtual to the base class destructor fixes the issue, ensuring both destructors are called correctly.

2. Abstract Classes and Pure Virtual Functions

Sometimes, you want to create a base class that only defines an interface but doesn't provide a meaningful implementation for its functions. For instance, a generic Shape class might not know how to calculate its area(), because that logic is specific to a Circle or a Rectangle.

In these cases, you can create a pure virtual function:
virtual float getArea() = 0;

The = 0 syntax tells the compiler that this function has no implementation in the base class. Any class that has one or more pure virtual functions becomes an abstract class.

Key properties of abstract classes:

  • You cannot create an instance of an abstract class. Shape myShape; would be a compile error.
  • Any class that derives from an abstract class must override all of its pure virtual functions. If it fails to do so, it also becomes an abstract class.

This is an extremely powerful tool for API design. It lets you enforce a contract: "any class that wants to be treated as a Shape must provide an getArea() function."

Polymorphism in C++ Programming - Startertutorials

This same resource also provides clear definitions for pure virtual functions and abstract classes.

Please read the sections "Pure Virtual Functions", "Late Binding (Dynamic Polymorphism)", and "Abstract Class". Focus on the syntax for a pure virtual function and the features and uses of an abstract class. The code example with Shape, Rectangle, and Circle is a perfect illustration of this pattern.

Conclusion

In this lesson, we took a significant step forward in our OOP journey. We moved from defining isolated classes to building structured hierarchies and flexible systems. Inheritance allows us to model "is-a" relationships and reuse code, while polymorphism, enabled by virtual functions, allows us to write generic code that operates on objects of many different specific types. These are not just abstract computer science concepts; they are the bread and butter of large-scale C++ applications, including the rendering engines and graphics tools you aim to master.

Key Takeaways:

  • Inheritance (class Derived : public Base) creates a hierarchy where a derived class inherits members from a base class, reducing code duplication.
  • The protected access specifier allows derived classes to access base class members while keeping them hidden from the outside world.
  • Polymorphism allows you to treat derived objects as instances of their base class, typically using base class pointers.
  • Virtual functions (virtual) enable dynamic dispatch (late binding), ensuring the correct derived class method is called at runtime. The override keyword should be used in the derived class for safety and clarity.
  • This mechanism is typically implemented using vtables and vptrs, which introduces a minor, but usually acceptable, overhead.
  • A base class destructor must be declared virtual to prevent memory leaks when deleting derived objects through a base pointer.
  • Pure virtual functions (= 0) create abstract classes, which define an interface that derived classes are required to implement.

In our next lesson, we will explore some of the most useful containers from the C++ Standard Template Library (STL), such as std::vector and std::unordered_map. You will see how the power of polymorphism is amplified when you store pointers to base class objects in these containers, allowing you to manage a collection of diverse scene objects with clean, uniform code.

Can't find a good explanation? Sign up and we'll make it for you

Sign up