Introduction
Hello! In our previous lesson, we explored how public inheritance allows us to model "is-a" relationships, creating hierarchies of classes like Circle "is-a" Shape. This is a powerful tool for code organization and reuse.
Today, we'll uncover one of the primary reasons inheritance is so fundamental in object-oriented programming: polymorphism. This concept, which literally means "many forms," allows us to interact with objects of different classes through a common interface.
This lesson will focus on explaining the crucial difference between the two main types of polymorphism in C++:
- Compile-time (or static) polymorphism, where decisions are made by the compiler before the program runs.
- Run-time (or dynamic) polymorphism, where decisions are made while the program is executing.
Understanding this distinction is key to making informed design choices that balance flexibility and performance in your applications.
1. What is Binding?
Before diving into polymorphism, we need to understand a core concept called binding. Binding is the process of associating a function call with the actual block of code (the function's definition) that will be executed. The question is: when does this association happen?
This "when" is what separates static and dynamic polymorphism. Let's watch a short video that defines these concepts clearly.
Static and Dynamic binding in C++ with examples (Early and Late binding)
This video from CodeBeauty provides excellent definitions for binding, static binding (compile-time), and dynamic binding (run-time). It also introduces their fundamental trade-offs.
Watch from 01:14 to 03:35. Focus on understanding: The definition of binding. When static vs. dynamic binding occurs. The primary trade-off: speed (static) vs. flexibility (dynamic).
In short:
- Static Binding (Early Binding): The association happens at compile time. The compiler knows exactly which function to call. This is fast and efficient.
- Dynamic Binding (Late Binding): The association happens at run time. The program decides which function to call based on the object's actual type during execution. This offers great flexibility.
2. Run-Time (Dynamic) Polymorphism
Dynamic polymorphism is the powerhouse of traditional object-oriented programming. It works through base class pointers or references and is enabled by the virtual keyword.
Imagine you have a base class pointer Shape*. This pointer could be pointing to a Circle object, a Rectangle object, or a Triangle object at any given moment. If you call a function like shapePtr->draw(), dynamic polymorphism ensures that the correct draw() function is called: Circle::draw(), Rectangle::draw(), or Triangle::draw(), depending on the object the pointer currently points to.
The decision is made at runtime, based on the dynamic type (the actual type of the object) rather than the static type (the declared type of the pointer/reference).

Let's see this in a practical example. The following video demonstrates dynamic polymorphism using a graphics Renderer that can be either an OpenGLRenderer or a VulkanRenderer.
Compile-time vs run-time polymorphism with C++
This video from constref shows a classic use case for dynamic polymorphism. An application can use any kind of renderer through a common base class interface.
Watch the first 4 minutes. Notice how the MainApplication holds a Renderer* pointer but doesn't know or care if it's an OpenGLRenderer or VulkanRenderer. The correct render() method is called automatically at runtime because it's a virtual function.
Under the Hood: V-Tables and V-Pointers
Your interest in how systems work "under the hood" is especially relevant here. C++ typically implements dynamic polymorphism using a mechanism called virtual tables (v-tables) and virtual pointers (v-ptrs).
- V-Table: For any class that has at least one
virtualfunction, the compiler creates a static array of function pointers. This is the v-table. Each entry in the table points to the address of one of the class's virtual functions. - V-Ptr: Each object of that class gets an extra, hidden member variable: the v-ptr. This pointer points to the v-table for its class.
When you make a virtual function call through a base pointer (e.g., shapePtr->draw()), the program performs two steps at runtime:
- It uses the object's v-ptr to find its class's v-table.
- It looks up the correct function address in the v-table and calls it.
This lookup is what "dynamic binding" is. It's an extra layer of indirection that adds a small performance overhead but provides immense flexibility.
This article from Nimrod's Coding Lab provides a concise explanation of the v-table and v-ptr mechanism.
Read the short section titled 'Under the Hood'. It explains exactly what v-tables and v-ptrs are and how they enable dynamic dispatch.
This runtime lookup, while fast, is not free. As the constref video mentions next, this overhead can be a concern in highly performance-sensitive code, like the main loop of a game engine.
3. Compile-Time (Static) Polymorphism
Static polymorphism achieves a similar goal—writing code that works with different types—but all decisions are resolved by the compiler. This avoids the runtime overhead of v-tables. The two primary ways to achieve this in C++ are function overloading and templates.
Function Overloading
You are already familiar with function overloading. When you provide multiple functions with the same name but different parameter lists, the compiler chooses the correct one at compile time based on the arguments you provide.
Static and Dynamic binding in C++ with examples (Early and Late binding)
Let's revisit the CodeBeauty video for a clear, simple demonstration of static binding through function overloading.
Watch from 04:45 to 08:06. The example with two sumNumbers functions is a perfect illustration of how the compiler uses the function signature to bind the call at compile time.
Templates
Templates are a more powerful form of static polymorphism. You can write a function or a class that is generic over one or more types. The compiler then generates specific versions of that function or class for each type you actually use it with.
For example, a template<typename T> void print(T value) function can work with int, double, std::string, etc., and the compiler generates the appropriate code for each, all before the program runs.
Let's look at an example that contrasts a templated (static) approach with the inheritance-based (dynamic) one.
Static vs. Dynamic Polymorphism
The article 'Static vs. Dynamic Polymorphism' presents a 'toy problem' and solves it using both dynamic and static polymorphism, making for a great side-by-side comparison.
Read the sections 'Dynamic polymorphism' and 'Static polymorphism'. Pay attention to how the Collatz class uses std::unique_ptr<Callable> (dynamic) while the T_Collatz class uses template parameters <class even, class odd> (static).
With the template version, the compiler knows the exact types of the _even_callable and _odd_callable objects at compile time. This means it can make a direct function call, and can even inline the function, which is a powerful optimization where the function's body is inserted directly at the call site, eliminating the overhead of a function call entirely. This is impossible with virtual functions.
4. The Trade-Off: A Concrete Comparison
So, when should you use one over the other? It comes down to flexibility vs. performance.
- Dynamic Polymorphism: Unbeatable when you need to decide on types at runtime. For example, processing a list of shapes where the user adds circles and squares, or selecting a plugin from a directory. The types are not known when you compile the code.
- Static Polymorphism: Ideal when you know all the types you'll be working with at compile time and performance is a top priority. The compiler can heavily optimize the code.
Let's see the performance impact directly.
Static vs. Dynamic Polymorphism
Let's return to the 'Static vs. Dynamic Polymorphism' article to see the results of their benchmark.
Now read the section 'The main function & timing results'. The code shows how both versions are used, and the timing results clearly demonstrate the performance advantage of the static approach for this problem.
As the results show, the static version was about twice as fast. In performance-critical loops, this difference can be substantial. This is the trade-off in action: the dynamic version could have hypothetically loaded callable objects from a config file at runtime, but the static version, with its types fixed at compile time, was significantly faster.
Conclusion
You now have a solid grasp of the two faces of polymorphism in C++. This is a critical concept for any advanced C++ developer.
Key Takeaways:
- Polymorphism means "many forms" and allows you to write a single interface that works with objects of different types.
- Binding is the process of linking a function call to its definition.
- Dynamic (Runtime) Polymorphism uses
virtualfunctions and base class pointers/references. Binding happens at runtime via v-tables. It's highly flexible but has a small performance cost. - Static (Compile-time) Polymorphism uses function overloading and templates. Binding happens at compile time. It's very performant (allowing for optimizations like inlining) but requires types to be known at compile time.
- The choice between them is a fundamental design decision based on the trade-off between runtime flexibility and compile-time performance.
Next Up
In this lesson, we learned that virtual functions enable dynamic polymorphism. In our next lesson, we will do a deep dive into the mechanics of using them correctly. We'll cover the virtual and override keywords, how virtual functions work in inheritance chains, and why they are essential for proper object-oriented design in C++.