AI InterviewSearch…Search problems, topics, or pages…Ctrl KGet started
Skip to content

10 Inheritance and polymorphism / Lesson 2 of 6

Virtual functions — static type, dynamic type and override

15 min · +10 XP · 2 exercises · 6 questions

Read the text15 min
Exercises0/2
Quiz6 q

Quick answer

What is a virtual function in C++?

A virtual function is a member function declared virtual in a base class, so that a call through a base reference or pointer runs the override belonging to the object's actual (dynamic) type, chosen at run time. Without virtual, the call is bound at compile time from the static type. Mark each override with override, and give the base a virtual destructor.

A Car bound to a Vehicle& is still a Car, but which describe() runs when you call it through the reference? Without virtual, the one Vehicle declared — the compiler chose it from the reference's type before the program ran. With virtual, the one Car provides — chosen at run time from the object's actual type. That single keyword is the difference between inheritance as code reuse and inheritance as polymorphism. This lesson covers what it changes: the two types every expression has, override and final, calling through a base reference, reaching the base version, and the two places where dispatch does not behave the way intuition says — default arguments and constructors.

Static type and dynamic type

Car car("mini", 3);
Vehicle& v = car;         // static type: Vehicle&   dynamic type: Car
Vehicle* p = &car;        // static type: Vehicle*   dynamic type: Car
Vehicle copy = car;       // static and dynamic type: Vehicle (slicing — lesson 4)

The static type is the declared type of the expression, known at compile time. The dynamic type is the type of the object it actually refers to, known only at run time and only meaningful for references and pointers — an object itself is exactly its declared type. A non-virtual call is resolved from the static type; a virtual call is resolved from the dynamic type. This is why polymorphism in C++ happens through references and pointers and never through values.

virtual

#include <iostream>
#include <string>

class Vehicle {
public:
    virtual ~Vehicle() = default;                                     // lesson 4 explains why
    virtual std::string describe() const { return "a vehicle"; }
    std::string plate() const { return "generic"; }                   // not virtual
};

class Car : public Vehicle {
public:
    std::string describe() const override { return "a car"; }
    std::string plate() const { return "car-plate"; }                 // hides, does not override
};

void show(const Vehicle& v) {
    std::cout << v.describe() << " / " << v.plate() << '\n';
}

int main() {
    Car car;
    show(car);                          // a car / generic
    std::cout << car.plate() << '\n';   // car-plate — on the object itself the static type is Car
}

describe is virtual, so v.describe() consults the dynamic type and finds Car::describe. plate is not, so v.plate() is bound at compile time to Vehicle::plate; Car::plate merely hides it (lesson 1) and is reached only through a Car. A function declared virtual in a base stays virtual in every derived class whether or not the keyword is repeated — Car::describe is virtual, and a SportsCar::describe would override it in turn.

Two costs come with the keyword. A virtual call is an indirect call through a table the compiler builds for the class (lesson 5 shows the mechanism), which the optimiser usually cannot inline; and every object of a class with at least one virtual function carries a hidden pointer to that table. Neither is large. Both are why the standard containers have no virtual functions and why virtual is a design decision rather than a default.

override and final

override says "this function overrides a virtual function in a base", and the compiler checks the claim. Without it, a mistake in the signature silently declares a new function, and the base version keeps running:

class Vehicle {
public:
    virtual std::string describe() const;
};
class Car : public Vehicle {
public:
    std::string describe() { return "a car"; }            // no const: a new function, not an override
    std::string describe() const override;                 // the real override
    std::string descrbe() const override;                  // error: marked 'override', but does not override
    std::string describe(int) const override;              // error: marked 'override', but does not override
};

A missing const, a typo, an extra parameter, a different return type — each compiles without override and fails quietly at run time. With it, every one is a compile error. Mark every override; GCC and Clang offer -Wsuggest-override to enforce the habit.

final closes the door: on a function, no further derived class may override it; on a class, nothing may derive from it at all.

class Taxi final : public Car {
public:
    std::string describe() const final { return "a taxi"; }
};
class Cab : public Taxi {};                                // error: cannot derive from 'final' base 'Taxi'

final documents a design decision, and it lets the compiler replace some virtual calls with direct ones, since it knows no override can exist below.

Calling the base version

An override often extends rather than replaces. The base version is reached with a qualified name, which is always a non-virtual call:

class Car : public Vehicle {
public:
    std::string describe() const override {
        return Vehicle::describe() + " with four wheels";     // "a vehicle with four wheels"
    }
};

Forget the qualifier — return describe() + … — and the function calls itself until the stack is exhausted.

Default arguments are bound statically

struct Base {
    virtual void greet(const std::string& who = "base") const { std::cout << "Base greets " << who << '\n'; }
};
struct Derived : Base {
    void greet(const std::string& who = "derived") const override { std::cout << "Derived greets " << who << '\n'; }
};

Derived d;
Base& b = d;
b.greet();          // Derived greets base

The function chosen is Derived::greet — dynamic dispatch — but the default argument comes from the static type, Base, because default arguments are filled in by the compiler at the call site, which only knows the static type. The result is a call nobody wrote. Rule: never redefine default arguments in an override; give them in the base only, or use an overload instead.

Virtual calls in constructors and destructors

struct Base {
    Base() { std::cout << name() << '\n'; }
    virtual ~Base() = default;
    virtual std::string name() const { return "Base"; }
};
struct Derived : Base {
    std::string name() const override { return "Derived"; }
};

Derived d;          // prints Base

During Base's constructor the object is a Base: the derived part has not been constructed yet, and C++ sets the dynamic type stage by stage as construction proceeds (lesson 5 explains how). So the virtual call resolves to Base::name, not to the override. Java does the opposite — it calls the override on a subclass whose fields are still zero — and C++'s choice is the safer one, but it surprises people arriving from Java. The same applies in destructors, in the other direction: by the time ~Base runs the derived part is gone and the dynamic type is Base again. A call to a pure virtual function (lesson 3) from a constructor has no body to reach and terminates the program.

If a derived class needs a hook to run after construction, provide a separate init() the creator calls, or use a factory function (Module 8, lesson 2) that constructs the object and then calls it.

Pitfalls

MistakeWhat happens
Calling a virtual function on a value rather than a reference or pointerThe static type is the dynamic type; no dispatch — and a copy may have sliced.
Override without override and a signature that differsA new, hidden function; the base version keeps running.
Different default arguments in an overrideThe base's default with the derived body.
Virtual call in a constructor expecting the derived versionThe base version runs.
describe() instead of Vehicle::describe() inside the overrideInfinite recursion.
Deleting through a base pointer with a non-virtual destructorUndefined behaviour — lesson 4.

Key takeaways

  • Static type: what the expression is declared as. Dynamic type: what the object really is. Non-virtual calls use the first, virtual calls the second.
  • Polymorphism happens only through references and pointers; a value has no separate dynamic type.
  • override turns a mismatched signature into a compile error; final forbids further overriding or deriving.
  • Base::f() calls the base version non-virtually — the way an override extends behaviour.
  • Default arguments are chosen from the static type; virtual calls inside constructors and destructors resolve to the class being constructed.

Common questions

What does the override keyword do in C++?

override tells the compiler that a function is meant to override a base class virtual function, and makes it a compile error if it does not — a missing const, a typo or a different parameter list. Without it, such a mistake silently declares a new function and the base version keeps running.

What is the difference between static type and dynamic type in C++?

The static type is the declared type of an expression, known at compile time; the dynamic type is the type of the object it actually refers to, known at run time. For Vehicle& v = car; they are Vehicle and Car. Non-virtual calls use the static type, virtual calls the dynamic one.

Can you call a virtual function from a constructor in C++?

You can, but it does not reach the derived override. While the base constructor runs, the object is still a Base — the derived part does not exist yet — so the call resolves to the base version. Destructors behave the same way in reverse, once the derived part is gone.

What does the final keyword do in C++?

On a virtual function, final forbids any further derived class from overriding it; on a class, as in class Taxi final, it forbids deriving from the class at all. It documents a design decision and lets the compiler turn some virtual calls into direct calls.

Why are default arguments not virtual in C++?

Default arguments are filled in by the compiler at the call site, which knows only the static type. So a call through a Base& runs the derived override but with the base's default argument. Never redefine default arguments in an override.

Mark as readSign in to keep your place.

C++ Virtual Functions Explained: override, final and Dispatch

A C++ virtual function is chosen at run time from the object's dynamic type, not the reference's static type. Override, final and the two dispatch traps.

  • Course: C++ study plan
  • Module: Inheritance and polymorphism
  • Kind: Lesson
  • Reading time: 15 min
  • Runtime: Clang 18 · C++20

What is a virtual function in C++?

A virtual function is a member function declared virtual in a base class, so that a call through a base reference or pointer runs the override belonging to the object's actual (dynamic) type, chosen at run time. Without virtual, the call is bound at compile time from the static type. Mark each override with override, and give the base a virtual destructor.

Lesson

A Car bound to a Vehicle& is still a Car, but which describe() runs when you call it through the reference? Without virtual, the one Vehicle declared — the compiler chose it from the reference's type before the program ran. With virtual, the one Car provides — chosen at run time from the object's actual type. That single keyword is the difference between inheritance as code reuse and inheritance as polymorphism. This lesson covers what it changes: the two types every expression has, override and final, calling through a base reference, reaching the base version, and the two places where dispatch does not behave the way intuition says — default arguments and constructors.

Static type and dynamic type

Car car("mini", 3);
Vehicle& v = car;         // static type: Vehicle&   dynamic type: Car
Vehicle* p = &car;        // static type: Vehicle*   dynamic type: Car
Vehicle copy = car;       // static and dynamic type: Vehicle (slicing — lesson 4)

The static type is the declared type of the expression, known at compile time. The dynamic type is the type of the object it actually refers to, known only at run time and only meaningful for references and pointers — an object itself is exactly its declared type. A non-virtual call is resolved from the static type; a virtual call is resolved from the dynamic type. This is why polymorphism in C++ happens through references and pointers and never through values.

virtual

#include <iostream>
#include <string>

class Vehicle {
public:
    virtual ~Vehicle() = default;                                     // lesson 4 explains why
    virtual std::string describe() const { return "a vehicle"; }
    std::string plate() const { return "generic"; }                   // not virtual
};

class Car : public Vehicle {
public:
    std::string describe() const override { return "a car"; }
    std::string plate() const { return "car-plate"; }                 // hides, does not override
};

void show(const Vehicle& v) {
    std::cout << v.describe() << " / " << v.plate() << '\n';
}

int main() {
    Car car;
    show(car);                          // a car / generic
    std::cout << car.plate() << '\n';   // car-plate — on the object itself the static type is Car
}

describe is virtual, so v.describe() consults the dynamic type and finds Car::describe. plate is not, so v.plate() is bound at compile time to Vehicle::plate; Car::plate merely hides it (lesson 1) and is reached only through a Car. A function declared virtual in a base stays virtual in every derived class whether or not the keyword is repeated — Car::describe is virtual, and a SportsCar::describe would override it in turn.

Two costs come with the keyword. A virtual call is an indirect call through a table the compiler builds for the class (lesson 5 shows the mechanism), which the optimiser usually cannot inline; and every object of a class with at least one virtual function carries a hidden pointer to that table. Neither is large. Both are why the standard containers have no virtual functions and why virtual is a design decision rather than a default.

override and final

override says "this function overrides a virtual function in a base", and the compiler checks the claim. Without it, a mistake in the signature silently declares a new function, and the base version keeps running:

class Vehicle {
public:
    virtual std::string describe() const;
};
class Car : public Vehicle {
public:
    std::string describe() { return "a car"; }            // no const: a new function, not an override
    std::string describe() const override;                 // the real override
    std::string descrbe() const override;                  // error: marked 'override', but does not override
    std::string describe(int) const override;              // error: marked 'override', but does not override
};

A missing const, a typo, an extra parameter, a different return type — each compiles without override and fails quietly at run time. With it, every one is a compile error. Mark every override; GCC and Clang offer -Wsuggest-override to enforce the habit.

final closes the door: on a function, no further derived class may override it; on a class, nothing may derive from it at all.

class Taxi final : public Car {
public:
    std::string describe() const final { return "a taxi"; }
};
class Cab : public Taxi {};                                // error: cannot derive from 'final' base 'Taxi'

final documents a design decision, and it lets the compiler replace some virtual calls with direct ones, since it knows no override can exist below.

Calling the base version

An override often extends rather than replaces. The base version is reached with a qualified name, which is always a non-virtual call:

class Car : public Vehicle {
public:
    std::string describe() const override {
        return Vehicle::describe() + " with four wheels";     // "a vehicle with four wheels"
    }
};

Forget the qualifier — return describe() + … — and the function calls itself until the stack is exhausted.

Default arguments are bound statically

struct Base {
    virtual void greet(const std::string& who = "base") const { std::cout << "Base greets " << who << '\n'; }
};
struct Derived : Base {
    void greet(const std::string& who = "derived") const override { std::cout << "Derived greets " << who << '\n'; }
};

Derived d;
Base& b = d;
b.greet();          // Derived greets base

The function chosen is Derived::greet — dynamic dispatch — but the default argument comes from the static type, Base, because default arguments are filled in by the compiler at the call site, which only knows the static type. The result is a call nobody wrote. Rule: never redefine default arguments in an override; give them in the base only, or use an overload instead.

Virtual calls in constructors and destructors

struct Base {
    Base() { std::cout << name() << '\n'; }
    virtual ~Base() = default;
    virtual std::string name() const { return "Base"; }
};
struct Derived : Base {
    std::string name() const override { return "Derived"; }
};

Derived d;          // prints Base

During Base's constructor the object is a Base: the derived part has not been constructed yet, and C++ sets the dynamic type stage by stage as construction proceeds (lesson 5 explains how). So the virtual call resolves to Base::name, not to the override. Java does the opposite — it calls the override on a subclass whose fields are still zero — and C++'s choice is the safer one, but it surprises people arriving from Java. The same applies in destructors, in the other direction: by the time ~Base runs the derived part is gone and the dynamic type is Base again. A call to a pure virtual function (lesson 3) from a constructor has no body to reach and terminates the program.

If a derived class needs a hook to run after construction, provide a separate init() the creator calls, or use a factory function (Module 8, lesson 2) that constructs the object and then calls it.

Pitfalls

MistakeWhat happens
Calling a virtual function on a value rather than a reference or pointerThe static type is the dynamic type; no dispatch — and a copy may have sliced.
Override without override and a signature that differsA new, hidden function; the base version keeps running.
Different default arguments in an overrideThe base's default with the derived body.
Virtual call in a constructor expecting the derived versionThe base version runs.
describe() instead of Vehicle::describe() inside the overrideInfinite recursion.
Deleting through a base pointer with a non-virtual destructorUndefined behaviour — lesson 4.

Key takeaways

  • Static type: what the expression is declared as. Dynamic type: what the object really is. Non-virtual calls use the first, virtual calls the second.
  • Polymorphism happens only through references and pointers; a value has no separate dynamic type.
  • override turns a mismatched signature into a compile error; final forbids further overriding or deriving.
  • Base::f() calls the base version non-virtually — the way an override extends behaviour.
  • Default arguments are chosen from the static type; virtual calls inside constructors and destructors resolve to the class being constructed.

Common questions

What does the override keyword do in C++?

override tells the compiler that a function is meant to override a base class virtual function, and makes it a compile error if it does not — a missing const, a typo or a different parameter list. Without it, such a mistake silently declares a new function and the base version keeps running.

What is the difference between static type and dynamic type in C++?

The static type is the declared type of an expression, known at compile time; the dynamic type is the type of the object it actually refers to, known at run time. For Vehicle& v = car; they are Vehicle and Car. Non-virtual calls use the static type, virtual calls the dynamic one.

Can you call a virtual function from a constructor in C++?

You can, but it does not reach the derived override. While the base constructor runs, the object is still a Base — the derived part does not exist yet — so the call resolves to the base version. Destructors behave the same way in reverse, once the derived part is gone.

What does the final keyword do in C++?

On a virtual function, final forbids any further derived class from overriding it; on a class, as in class Taxi final, it forbids deriving from the class at all. It documents a design decision and lets the compiler turn some virtual calls into direct calls.

Why are default arguments not virtual in C++?

Default arguments are filled in by the compiler at the call site, which knows only the static type. So a call through a Base& runs the derived override but with the base's default argument. Never redefine default arguments in an override.

Exercises

Which function ran

Write Animal with a virtual std::string sound() const returning Animal::sound and a non-virtual std::string describe() const returning Animal::describe. Write Dog and Cat deriving publicly from it; each overrides sound (marked override) and also declares its own describe — which, being non-virtual, only hides the base's. Every function returns its own qualified name as a string, so the output tells you which one ran.

The starter's viaBase takes a const Animal& and calls both functions through it. For each kind, print one line through the base reference and one line calling directly on the object.

Input: n, then n kinds: dog, cat or animal. Output: two lines per kind.

2
dog
animal

prints

dog via Animal&: Dog::sound, Animal::describe
dog direct: Dog::sound, Dog::describe
animal via Animal&: Animal::sound, Animal::describe
animal direct: Animal::sound, Animal::describe

Pricing policies with override and final

Write a small hierarchy of pricing policies and apply each one to a list of prices.

  • Pricing has a virtual destructor and virtual long long price(long long base) const returning base unchanged.
  • Percent(int p) overrides it: base - base * p / 100 (integer arithmetic).
  • Flat(long long off) overrides it: base - off, but never below 0.
  • Clamped(int p, long long floor) derives from Percent, is marked final, and overrides price as the larger of Percent::price(base) — call the base version by its qualified name — and floor.

Mark every override override. The starter's makePolicy returns a std::unique_ptr<Pricing> for a command line; complete it.

Input: m, then a line of m prices, then one policy per line until the input ends: none, percent p, flat off or clamped p floor. Output: one line per policy: the policy line as given, a colon, then each price after the policy.

3
100 250 40
none
percent 10
flat 30
clamped 50 30

prints

none: 100 250 40
percent 10: 90 225 36
flat 30: 70 220 10
clamped 50 30: 50 125 30

In this module: Inheritance and polymorphism

← Inheritance basics — deriving, protected and the order of construction · Abstract classes and interfaces — programming to a contract →