Menu
Coddy logo textTech

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 icon

Challenge

Easy

Let'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 base Calculator class that represents any calculation strategy:
    • A virtual calculate(int a, int b) method that returns an int and prints: Base calculation: <a> ? <b> (returning 0)
    • A virtual destructor
  • Operations.h: Define two derived calculator classes that override the calculation behavior:
    • Adder: Override calculate() to print Adding: <a> + <b> and return the sum
    • Multiplier: Override calculate() to print Multiplying: <a> * <b> and return the product
    Both classes should use the override keyword.
  • 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 base Calculator, an Adder, and a Multiplier. Loop through and call calculate() 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: 30

Notice 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;
}
quiz iconTest yourself

This lesson includes a short quiz. Start the lesson to answer it and track your progress.

All lessons in Object Oriented Programming