Compile vs Runtime Polymorph
Part of the Object Oriented Programming section of Coddy's C++ journey — lesson 56 of 104.
Polymorphism means "many forms" and is a core OOP concept that allows objects to be treated uniformly while behaving differently. C++ supports two distinct types of polymorphism, each resolved at a different stage of program execution.
Compile-time polymorphism (also called static polymorphism) is resolved by the compiler before the program runs. The compiler determines exactly which function to call based on the function signature. This includes function overloading and templates:
void print(int x) { std::cout << "Integer: " << x << std::endl; }
void print(double x) { std::cout << "Double: " << x << std::endl; }
print(5); // Compiler chooses print(int)
print(3.14); // Compiler chooses print(double)Runtime polymorphism (also called dynamic polymorphism) is resolved while the program is running. The decision about which function to call depends on the actual object type, not the pointer or reference type. This is achieved through virtual functions:
class Shape {
public:
virtual void draw() { std::cout << "Drawing shape" << std::endl; }
};
class Circle : public Shape {
public:
void draw() override { std::cout << "Drawing circle" << std::endl; }
};
Shape* s = new Circle();
s->draw(); // Decided at runtime: "Drawing circle"The key trade-off: compile-time polymorphism has zero runtime overhead since decisions are made during compilation, while runtime polymorphism adds a small cost (vtable lookup) but provides greater flexibility for working with objects whose types aren't known until execution.
Challenge
EasyLet's build a calculator system that demonstrates both types of polymorphism side by side. You'll create a system where compile-time polymorphism handles different input types through function overloading, while runtime polymorphism allows different calculation strategies to be swapped dynamically.
You'll organize your code across three files:
Calculator.h: Define a baseCalculatorclass that represents any calculation strategy:- A virtual
calculate(int a, int b)method that returns anintand prints:Base calculation: <a> ? <b>(returning 0) - A virtual destructor
- A virtual
Operations.h: Define two derived calculator classes that override the calculation behavior:Adder: Overridecalculate()to printAdding: <a> + <b>and return the sumMultiplier: Overridecalculate()to printMultiplying: <a> * <b>and return the product
overridekeyword.main.cpp: Create a system that showcases both polymorphism types. Read two integer inputs (each on a separate line).First, demonstrate compile-time polymorphism by creating three overloaded
display()functions:display(int x)prints:Integer value: <x>display(double x)prints:Double value: <x>display(const std::string& x)prints:String value: <x>
Then demonstrate runtime polymorphism by creating an array of
Calculator*pointers containing a baseCalculator, anAdder, and aMultiplier. Loop through and callcalculate()on each with your input values, printing the result after each calculation.Structure your output as follows:
=== Compile-Time Polymorphism === <display outputs for int, double, string> === Runtime Polymorphism === <calculate outputs with results>For the compile-time section, call
display()with the first input as an integer, then as a double (same value with .5 added), then as the string "Result". Clean up your dynamically allocated calculators when done.
For example, with inputs 10 and 3:
=== Compile-Time Polymorphism ===
Integer value: 10
Double value: 10.5
String value: Result
=== Runtime Polymorphism ===
Base calculation: 10 ? 3
Result: 0
Adding: 10 + 3
Result: 13
Multiplying: 10 * 3
Result: 30Notice how the compiler selects the correct display() overload based on the argument type (compile-time decision), while the correct calculate() method is determined by the actual object type at runtime through the vtable mechanism.
Cheat sheet
C++ supports two types of polymorphism resolved at different stages:
Compile-time polymorphism (static polymorphism) is resolved by the compiler before execution. Includes function overloading and templates:
void print(int x) { std::cout << "Integer: " << x << std::endl; }
void print(double x) { std::cout << "Double: " << x << std::endl; }
print(5); // Compiler chooses print(int)
print(3.14); // Compiler chooses print(double)Runtime polymorphism (dynamic polymorphism) is resolved during execution using virtual functions. The actual object type determines which function is called:
class Shape {
public:
virtual void draw() { std::cout << "Drawing shape" << std::endl; }
};
class Circle : public Shape {
public:
void draw() override { std::cout << "Drawing circle" << std::endl; }
};
Shape* s = new Circle();
s->draw(); // Decided at runtime: "Drawing circle"Trade-off: Compile-time polymorphism has zero runtime overhead, while runtime polymorphism adds a small cost (vtable lookup) but provides greater flexibility.
Try it yourself
#include <iostream>
#include <string>
#include "Calculator.h"
#include "Operations.h"
// TODO: Create three overloaded display() functions:
// 1. display(int x) - prints "Integer value: <x>"
// 2. display(double x) - prints "Double value: <x>"
// 3. display(const std::string& x) - prints "String value: <x>"
int main() {
// Read two integer inputs
int a, b;
std::cin >> a;
std::cin >> b;
// === Compile-Time Polymorphism ===
std::cout << "=== Compile-Time Polymorphism ===" << std::endl;
// TODO: Call display() with:
// - a as an integer
// - a as a double (add 0.5 to it)
// - the string "Result"
std::cout << std::endl;
// === Runtime Polymorphism ===
std::cout << "=== Runtime Polymorphism ===" << std::endl;
// TODO: Create an array of Calculator* pointers with 3 elements:
// - a base Calculator
// - an Adder
// - a Multiplier
// TODO: Loop through the array, call calculate(a, b) on each,
// and print "Result: <return_value>" after each calculation
// TODO: Clean up dynamically allocated memory
return 0;
}
This lesson includes a short quiz. Start the lesson to answer it and track your progress.
All lessons in Object Oriented Programming
1Fundamentals of OOP
External FilesC++ Build & CompilationHeader Files & Source FilesNamespaces & ScopeIntroduction to OOP in C++Classes vs ObjectsThe 'this' PointerMethods (Member Functions)Attributes (Data Members)Ctors & Dtors BasicsRecap - Simple Calculator4Class Properties
Instance vs Static MembersGetters and SettersConst Member FunctionsMutable KeywordStatic Methods and VariablesFriend Functions & ClassesRecap - Bank Account Manager7Inheritance
Basic InheritanceInheritance Access LevelsCtor & Dtor Call OrderMethod OverridingVirtual Functions & VTableMultiple InheritanceVirtual InheritanceRecap - Employee Hierarchy2Memory Management
Stack vs Heap MemoryPointers and ReferencesDynamic Memory (new/delete)Smart Pointers in C++RAII in C++Recap - Dynamic Array Manager5Encapsulation
Access Specifiers in C++Access Specifiers In DepthInformation HidingStruct vs ClassNested & Inner ClassesRecap - Student Records System8Polymorphism
Compile vs Runtime PolymorphFunction OverloadingVirtual Functions RevisitedPure Virtual FunctionsAbstract ClassesInterface Design in C++Dynamic Casting & RTTIRecap - Shape Calculator3Constructors & Destructors
Default ConstructorParameterized ConstructorCopy ConstructorMove ConstructorConstructor Init ListsDelegating ConstructorsDestructor Deep DiveRule of Three / Five / ZeroRecap - String Class6Operator Overloading
Intro to Operator OverloadArithmetic Operator OverloadComparison Operator OverloadStream OperatorsAssignment Operator Overload[] and () Operator OverloadType Conversion OperatorsRecap - Matrix Class9Templates
Function TemplatesClass TemplatesTemplate SpecializationVariadic TemplatesSFINAE & Type Traits BasicsRecap - Generic Container