Understanding Static Types, Dynamic Types, and Polymorphism in C++

Static Type vs Dynamic Type

A static type is the type declared for an object at compile time. Once the compiler processes the declaration, the static type is fixed and never changes.

A dynamic type refers to the actual type of the object that a pointer or reference points to. Since the compiler cannot know the actual runtime type, the dynamic type is only determined during program execution. Only pointers and references have dynamic types.

Item* ptr = new DiscountItem;  // Item is the base class, DiscountItem is derived

In this example, the static type of ptr is Item, resolved during compilation. However, its dynamic type is DiscountItem, discovered only at runtime.

Virtual Functions and Call Resolution

class Parent {
public:
    Parent() {}
    
    virtual void display() {
        std::cout << "Parent display" << std::endl;
    }
    
    void show() {
        std::cout << "Regular function" << std::endl;
    }
};


class Child : public Parent {
public:
    Child() {}
    
    void display() {
        std::cout << "Child display" << std::endl;
    }
    
    void show() {
        std::cout << "Child's regular function" << std::endl;
    }
};

int main() {
    Parent* parent = new Child;
    parent->display();
    
    Parent* parent1 = new Child;
    parent1->show();
}

When the compiler encounters parent->display(), it performs several checks in order:

  1. Verifies the static type of parent (which is Parent) and checks whether that class declares a display() function
  2. If the function doesn't exist in the static type, compilation fails
  3. If found, the compiler checks weather the call involves a pointer or reference performing an upcast (base pointer/reference pointing to derived object)
  4. The compiler also verifies whether display() is declared as virtual

If any of these conditions fail, the call resolves to the static type's function. When all conditions are met, the call is deferred to runtime.

At runtime, the system examines the dynamic type through the object's virtual table pointer (stored in the first 4 bytes of the object). This vtable contains addresses of virtual functions. The correct function is located by indexing into this table, with O(1) lookup complexity.

Null Pointer Behavior with Virtual Functions

Since display() is virtual and parent is a pointer, the function must be resolved at runtime based on the actual object type. If the pointer is null but the function call proceeds through the vtable mechanism, undefined behavior occurs—tyipcally a crash or exception.

In contrast, show() is not virtual. The compiler resolves this call entirely at compile time using the static type, so no runtime resolution is needed and no error occurs.

Default Parameters in Polymorphic Functions

A common pitfall: derived classes should not override non-virtual functions from the base class. Additionally, when a base class virtual function provides a default argument and a derived class overrides it with a different default, polymorphism always uses the base class's default value.

class Base {
public:
    Base() {}
    
    virtual void execute(int value = 5) {
        std::cout << "Base execute with " << value << std::endl;
    }
};

class Derived : public Base {
public:
    Derived() {}
    
    void execute(int value = 100) {
        std::cout << "Derived execute with " << value << std::endl;
    }
};

int main() {
    Base* instance = new Derived;
    instance->execute();
}

When instance->execute() is called, the actual function invoked is from Derived (due to polymorphism), but the default argument used is 5 from Base, not 100 from Derived. This occurs because default arguments are resolved statically at compile time based on the static type.

Tags: C++ static type dynamic type Polymorphism virtual function

Posted on Fri, 03 Jul 2026 17:26:12 +0000 by ben2.0