Skip to content
VLSI Mentor

C++ Part I · Module 17 · Runtime Polymorphism

What Runtime Polymorphism Buys You

Module 16 built hierarchies that could not do anything. A derived object had a base part at a known offset, a derivation asserted substitutability, and construction ran in four phases — but a function taking a base reference still called the base class's functions, whatever object it had been handed, which Chapter 16.3 measured and Chapter 16.6 explained. That is the gap this module closes, and it is worth being precise about what is missing, because the answer is not simply more syntax. What a hierarchy cannot yet do is let one piece of code work with a type that did not exist when that code was written. This chapter establishes that capability and measures it in the strongest form available: a client translation unit compiled to an object file, hashed, and then linked unchanged into a program containing a new implementation it had never seen — same object code, same hash, driving a model type added afterwards. Then the distinction the frozen description of this chapter asks for, which catches people out because both mechanisms are called polymorphism and only one of them happens at run time: overloading picks a function from a set the compiler can see, and this module is about the case where the set is not knowable until the program is running. And the single size measurement that accounts for what the capability costs an object, with the rest of the cost deferred to the chapter that measures it properly.

Intermediate17 min readC++Part IFundamentalsPolymorphism

Part I · Module 17 · Chapter 17.1 · What Runtime Polymorphism Buys You

1. The Problem

A simulation holds a reference model. Later it needs to hold a fast approximate model instead, or to run both and compare, or to accept one a different team supplies. The code that drives the model — steps it, reads its outputs, logs them — should not change when the model does.

Write it the obvious way and it does change:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    void drive(ReferenceModel& m);          // works for exactly one type

Every new model means a new overload, a new branch, or a template instantiation — and every one of them means editing and recompiling the driver. The problem is not verbosity. It is that the driver has to be edited whenever the set of models changes, which makes the set of models a property of the driver.

The requirement: write code once, against an interface, and have it work with implementations chosen later — including after that code is compiled.

2. What Module 16 Could Not Do

Chapter 16.3 §3 measured the gap. With ordinary member functions, a call through a base reference reaches the base's function:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
A. NON-virtual, through a base reference:
     PlainBase::run

The object was a PlainDerived. The reference was a PlainBase&. The base version ran, because the compiler chose by the reference's static type — Chapter 16.6's name hiding. So a driver taking PlainBase& can only ever execute PlainBase's behaviour, which makes the hierarchy decorative.

3. Not the Same as Overloading

Both are called polymorphism, and the distinction is the one worth fixing first.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    void log(int);                  // overloading: the compiler picks from
    void log(const char*);          // a set it can see, at compile time
    void log(double);

Chapter 6.7 established how that choice is made: lookup produces a candidate set, resolution ranks it, and the decision is baked into the generated code. The set of candidates is fixed when the translation unit is compiled.

OverloadingRuntime polymorphism
decidedcompile timerun time
bythe argument types the compiler seesthe object's actual type
candidate setfixed at compile timeopen — extended later
needs a hierarchynoyes

The practical difference is §5's. Overloading cannot help with a type that does not exist yet, because the compiler cannot rank a candidate it has never seen.

(There is a third kind — compile-time polymorphism via templates, where one piece of code works with many types but the set is still fixed when the code is instantiated. That is Module 18, and Chapter 18.1 compares the two properly.)

4. What Changes With One Keyword

Declare the function virtual in the base and the same call behaves differently:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    struct VBase    { virtual ~VBase() = default; virtual void run() const { /* ... */ } };
    struct VDerived : VBase { void run() const override { /* ... */ } };

    static void driveV(const VBase& b) { b.run(); }
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
B. VIRTUAL, through a base reference:
     VDerived::run
C. the same virtual call through a base POINTER:
     VDerived::run

The derived implementation ran, through a reference and through a pointer. The expression's static type is still VBase; what changed is that the call now consults the object rather than the expression.

And one function serves every type in the hierarchy:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
D. one function, three objects, three implementations:
     VBase::run
     VDerived::run
     VOther::run

One driveV, three objects, three behaviours, no branching. (How the call finds the right implementation is Chapter 17.2, which measures the three instructions it takes.)

5. The Measurement That Matters

§4 shows dispatch working within one program. The claim worth testing is stronger: that code compiled before an implementation exists can drive it.

Four files. A Model interface, a client that uses it, and two versions of an implementation file — the second adding a FastModel the client has never heard of:

Azvya Education Pvt. Ltd.VLSI Mentor
client.cpp — compiled once, never edited
    #include "client.h"
    void runTwice(Model& m) { m.step(); m.step(); std::printf("     ran %s twice\n", m.name()); }

Compile the client to an object file, twice, to establish the baseline:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
sha256 of client.o:
   2ee7c682aa40fd78c059972f4d6f939ad5f7cf8a25fa8956a090832e2e38fc17  client_a.o
   2ee7c682aa40fd78c059972f4d6f939ad5f7cf8a25fa8956a090832e2e38fc17  client_b.o

Now link that same object file against each implementation version:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
=== link against V1 (one implementation) ===
     ran ReferenceModel twice
     ticks=2
=== link the SAME client_a.o against V2 (a new implementation added) ===
     ran ReferenceModel twice
     ticks=2
     ran FastModel twice
     ticks=20

client.cpp was not recompiled, re-read, or edited. The identical client_a.o drove a FastModel that did not exist when it was compiled, and the ticks=20 proves FastModel::step ran rather than the interface's.

That is the capability, stated as precisely as it can be:

The client's machine code is independent of the set of implementations. New implementations are added without touching it.

Chapter 17.10 is the chapter on writing that client side properly; this measurement is what makes it worth doing.

6. What It Costs the Object

One number here; the rest belongs to the chapter that measures it properly.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
E. sizeof(PlainBase)=1  sizeof(PlainDerived)=1
   sizeof(VBase)=8      sizeof(VDerived)=8

An empty class is 1 byte; adding a virtual function makes it 8. The object now carries a hidden pointer — Chapter 17.7 explains what it points at and measures the per-call cost, which turns out to be smaller and differently distributed than most people expect.

That second claim — one pointer, however many virtual functions — is worth measuring rather than asserting:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  sizeof(Empty)   = 1
  sizeof(One)     = 8   (1 virtual + dtor)
  sizeof(Five)    = 8   (5 virtuals + dtor)
  sizeof(Twenty)  = 8   (20 virtuals + dtor)
  sizeof(Both)    = 16   (TWO polymorphic bases)

One, five and twenty virtual functions all cost the same eight bytes. The object carries a pointer to a per-class table, so the number of entries in that table is a property of the class, not of each object. Twenty virtual functions cost twenty table slots once, program-wide, and still one pointer per object.

The last row is the exception: a class with two polymorphic bases carries two pointers, 16 bytes, because it has two base subobjects each needing to answer dispatch questions — which is Chapter 16.2 §4's note about a second base not being at offset 0, seen from the other side.

Two things follow. A class with any virtual function is at least a pointer in size, so virtual on a type you create in millions is a real decision — an object of four ints doubles. And the cost is per object, not per call site and not per function, which makes a wide interface on few objects cheap and a narrow interface on many objects the case to think about.

7. Where It Is the Right Answer

The pattern is always the same: one piece of code, several implementations, the choice made at run time.

SituationThe interface
a reference model and a fast approximate modelModel with step()
several bus protocols behind one checkerTransaction with kind(), addr()
a scoreboard that accepts any comparatorComparator with matches()
output to a file, a socket, or a test bufferSink with write()
a plugin loaded from configurationwhatever the plugin implements

And where it is not the answer: when the set of types is small, fixed, and known at compile time, a template (Module 18) or a plain function does the job with no indirection. Reaching for a hierarchy because it feels more object-oriented is how the bad hierarchies of Chapter 16.8 get built.

(The module's blurb names three classic failures, and all three are in this module: the missing virtual destructor — Chapter 17.5 — the sliced object — Chapter 17.6 — and the interface that was never abstract — Chapter 17.4. The last is the one this chapter's VBase is guilty of: it has a working run(), so a VBase can be instantiated and the interface is a suggestion rather than a contract.)

8. What This Module Will Add

In order, and each resolving something this chapter has left open:

ChapterThe question it answers
17.2how does the call find the right implementation?
17.3how do I stop a signature typo from silently creating a new function?
17.4how do I state a contract that must be implemented?
17.5what leaks when I delete through a base pointer?
17.6what happens if I copy a derived object into a base object?
17.7what does a virtual call actually cost?
17.8can I ask what type an object really is, and should I?
17.9what makes a base class worth implementing?
17.10how do I write the code that uses one?

9. What to Actually Do

Use runtime polymorphism when the set of implementations is open — chosen or added after your code is compiled.

Distinguish it from overloading — same word, different time, and overloading cannot reach a type it has never seen.

Expect one pointer per object — measured, 1 byte becoming 8.

Do not reach for a hierarchy when the type set is fixed and small — a template or a plain function is cheaper and clearer.

And ask the §5 question before the syntax question: will already-compiled code need to drive something added later?

10. Mistakes Worth Making Once

Building a hierarchy and expecting dispatch without virtual. Measured: the base version ran.

Confusing overloading with runtime polymorphism. One is resolved at compile time from a closed set.

Expecting a template to solve the open-set problem. Instantiation still needs the type at compile time.

Adding virtual to a type you create in millions. 1 byte becomes 8, per object.

Thinking the cost is per call site. It is per object — one pointer regardless of how many virtual functions.

Using a hierarchy for three types you wrote and will not change. Chapter 16.8's catalogue.

Leaving the base instantiable when it was meant as a contract — Chapter 17.4.

11. Interview Relevance

"What problem does runtime polymorphism solve?" Code that must work with a type chosen later — including after that code is compiled. The strong answer makes the after compilation part explicit.

"How is it different from overloading?" Overloading is resolved at compile time from a candidate set the compiler can see; this is resolved at run time from the object, and the set is open.

"Can you demonstrate the difference?" The sharpest demonstration is that a client object file, unchanged and byte-identical, can drive an implementation added afterwards.

"What does it cost?" One pointer per object — measured, 1 byte becoming 8 — plus a per-call cost that Chapter 17.7 measures as mostly lost inlining rather than indirection.

"When would you not use it?" When the set of types is fixed and known at compile time; a template keeps the abstraction without the indirection.

"Why is a non-virtual hierarchy not polymorphic?" Because the call is resolved by the static type — measured as the base version running through a base reference.

12. What You Should Be Able to Say Now

  1. The requirement is code that works with implementations chosen later, not merely shared code.
  2. Measured: without virtual, a call through a base reference ran PlainBase::run on a derived object.
  3. Overloading is resolved at compile time from a candidate set the compiler can see, and is baked into the generated code.
  4. Runtime polymorphism is resolved from the object, and the implementation set is open.
  5. A template fixes the set at instantiation, so it does not solve the open-set problem — Module 18.
  6. Measured: with virtual, the same call ran VDerived::run through both a reference and a pointer.
  7. Measured: one function drove three objects to three implementations with no branching.
  8. Measured: a byte-identical client.o (sha256 2ee7c682…) drove a FastModel that did not exist when it was compiled.
  9. So the client's machine code is independent of the implementation set.
  10. Measured: an empty class is 1 byte; with a virtual function it is 8.
  11. The cost is per object, not per call site — one pointer however many virtual functions.
  12. The trade: you give up compile-time knowledge of which function runs, and gain an open set.
  13. Where the set is genuinely fixed, a template or a plain function is cheaper.
  14. The module's three classic failures are the missing virtual destructor, the sliced object, and the interface that was never abstract — Chapters 17.5, 17.6 and 17.4.

13. Exercises

Exercise 1 — state the requirement. Describe, without using the word virtual, what capability this module adds.

Exercise 2 — predict the call. A Base& bound to a Derived, calling run(). Give the result with and without virtual, and name the mechanism in the non-virtual case.

Exercise 3 — the two polymorphisms. Fill in the four-row comparison table, then say which one can reach a type that does not yet exist.

Exercise 4 — design the measurement. Describe an experiment proving a client's code is independent of the implementation set, and say what you would hash.

Exercise 5 — the size. Give sizeof for an empty class, the same class with one virtual function, and with five virtual functions. Explain the third.

Exercise 6 — open or closed. For each, say whether the implementation set is open: a reference model and a fast model you both wrote; bus protocols added by other teams; three shapes in a geometry library; an output sink chosen by a config file; a comparator for std::sort.

Exercise 7 — the wrong tool. Give a case where a hierarchy is the wrong answer and say what you would use instead.

Exercise 8 — the third failure. Explain why this chapter's VBase is an example of "the interface that was never abstract".

14. Summary

  • The requirement is code that works with implementations chosen later — including after it is compiled.
  • Measured: without virtual, a base reference ran the base's function on a derived object — a decorative hierarchy.
  • Overloading is a compile-time choice from a closed set; runtime polymorphism is a run-time choice from an open one.
  • Measured: with virtual, one function drove three objects to three implementations.
  • Measured, the headline: a byte-identical client.o drove a FastModel added after it was compiled — ticks=20 proving the derived implementation ran.
  • Measured: the object cost is 1 byte becoming 8 — one pointer per object, not per call site.
  • The trade is compile-time knowledge for an open implementation set, and it is only worth it where the set is genuinely open.
  • Three classic failures follow in this module: the missing virtual destructor, the sliced object, and the interface that was never abstract.

The capability is established and the mechanism is still a black box. §4 showed a call through a base reference reaching the derived implementation, and §6 showed the object growing by exactly one pointer — which is the whole clue, because the information that makes the call work has to live in the object rather than at the call site. The next chapter opens the box and measures what a virtual call actually does: at -O2 it is three instructions, and the middle one reads a function address out of a table at a fixed offset chosen when the call site was compiled. That is the entire mechanism, it explains the one pointer, and it explains why the same call site can reach an implementation that did not exist at the time — the offset is fixed, and what sits at that offset is not.

Standards & specifications

Governing standard
ISO/IEC 14882 (the C++ language standard)(opens ISO/IEC (JTC 1/SC 22/WG 21) in a new tab)

Defines the C++ language and its standard library — the type system and templates, the object model and object lifetime, initialisation and overload resolution, the memory model and the containers, algorithms and utilities the library provides. Like C's standard it is a FAMILY of editions (C++98/03, C++11, C++14, C++17, C++20, C++23) and the editions are NOT interchangeable: a feature this curriculum relies on may be unavailable, differently specified or merely proposed in another edition, so each lesson names the edition its claim depends on. It does not define any foreign-function interface to a hardware description language, nor what a compiler actually emits, nor platform behaviour on real silicon — those are the implementation's and the device's contracts. Working drafts published by WG21 are freely available and are what most engineers read day to day; a draft is not the published standard.

This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.

Where this fits

Part of the C++ Programming curriculum.