Object-Oriented Concepts
Imagine building a graphics editor that must draw circles, rectangles, and text boxes. A designer keeps adding new shapes every month. A purely imperative design forces every drawing routine to branch on a shape-tag: `if kind == CIRCLE ... else if kind == RECT ...`. Each new shape means editing every such routine. These routines are scattered across the codebase. This risks regressions in code nobody wants to touch again. Object-oriented programming was invented to invert this dependency. Instead of routines branching on data, data carries its own behavior. Adding a new shape means adding one new unit of code. That unit plugs into the existing system unchanged. This chapter dissects the four ideas that make that inversion possible: objects as bundles of state and behavior, encapsulation as an enforced boundary around that state, inheritance as a mechanism for sharing and specializing behavior, and dynamic dispatch as the runtime machinery that picks the right behavior for the right object. These ideas did not arrive as a single clean theory. They accreted from Simula's simulation needs, Smalltalk's message-passing philosophy, and C++/Java's performance-driven compromises. Understanding the tension between them explains why modern languages look the way they do. The big picture is that object orientation is fundamentally about late-bound extensibility: pushing the decision of "which code runs" from compile time toward run time, and from the caller's knowledge to the callee's own declared type. This buys enormous flexibility (plugins, frameworks, polymorphic containers) at a real cost (harder-to-predict control flow, virtual-call overhead, the fragile base class problem). A working grasp of Names, Binding, and Scope is essential background. Dynamic dispatch is precisely a binding-time decision. It defers name resolution for methods to run time rather than compile time. This contrasts with the static binding you already know from The Imperative Paradigm. This article delivers a precise vocabulary for objects, classes, and encapsulation. It gives a step-by-step account of how virtual method tables implement dispatch. It offers a worked trace through a small class hierarchy showing exactly which table entry gets called and why. It also gives an honest accounting of where the technique helps and where it hurts.
- An object bundles state (fields) with behavior (methods) that operate on that state.
- Encapsulation hides internal representation behind a public interface, enforcing an invariant boundary.
- Inheritance lets a subclass reuse and specialize a superclass's fields and methods.
- Dynamic dispatch resolves which method body runs based on an object's run-time type, not its declared type.
- Dispatch is typically implemented with a virtual method table (vtable): one indirection, O(1) call.
- The cost of flexibility is subtler control flow, the fragile base class problem, and dispatch overhead.
Object-oriented programming defers the choice of which code runs from compile time to run time by binding method calls to an object's dynamic type rather than its static type.
The idea
An object is a unit that packages state (a set of fields, or instance variables) together with behavior (a set of methods) that is permitted to read and modify that state. A class is a template — a compile-time description of the fields and methods that every object created from it (every instance) will have. Creating an instance (`new Circle(5)`) allocates storage for the fields and associates the object with its class, so that later, when a method is invoked on the object, the language runtime knows which code to run.
Encapsulation is the discipline of hiding an object's representation behind a well-defined interface, exposing only the operations a client is permitted to use. Formally, encapsulation establishes a representation invariant — a property of the internal state (e.g., "the linked list's `size` field always equals the number of nodes reachable from `head`") that holds after every public operation completes, and that only the class's own methods are trusted to preserve. Access modifiers (`private`, `protected`, `public`) are the syntactic mechanism; the invariant is the semantic payoff. Encapsulation is what lets a class change its internal implementation (say, from an array to a hash table) without breaking client code, as long as the public interface's behavior is preserved — this is the same idea as an abstract data type, made concrete with bundled behavior.
Inheritance lets a class (the subclass or derived class) declare that it extends another class (the superclass or base class), automatically acquiring its fields and methods, and then adding new members or overriding (replacing) some of the inherited methods with more specific behavior. Inheritance serves two distinct purposes that are worth naming separately: implementation reuse (don't re-write code already written in the base class) and subtyping (a `Circle` is a `Shape`, so anywhere a `Shape` is expected, a `Circle` may be substituted). The Liskov Substitution Principle states this precisely: if `S` is a subtype of `T`, objects of type `T` in a program may be replaced by objects of type `S` without altering any desirable property of that program. Violating this principle — e.g., a `Square` subclass of `Rectangle` that breaks `setWidth`/`setHeight` independence — is a classic design trap, not a language bug, but the type system cannot catch it automatically.
Dynamic dispatch (also called late binding or virtual dispatch) is the mechanism that makes overriding meaningful: when code calls `shape.draw()`, the method body that executes is chosen based on the dynamic type (run-time type) of the object bound to `shape`, not the static type (compile-time declared type) of the variable. This is the crux of the whole paradigm. Contrast it with static dispatch (ordinary function calls, and overload resolution in most languages), which is fully resolved at compile time using only static type information — exactly the binding-time distinction studied for variable names in Names, Binding, and Scope, now applied to method names.
How it works
Dynamic dispatch must answer, at every call site, the question: "given this object, which method body runs?" The answer must be computed fast, because virtual calls are common in idiomatic OO code, and it must be computed correctly even when the object's exact class is unknown until run time (it might have been constructed behind a factory, deserialized, or passed in from a plugin). The standard solution, used by C++, Java, C#, and most statically-typed OO languages, is the virtual method table (vtable).
The layout. Each class (not each object) gets exactly one vtable: an array of function pointers, one slot per virtual method, ordered consistently across the whole hierarchy so that a method's slot index is the same in every subclass. Each object carries one extra hidden field — a pointer to its class's vtable — set once at construction time and never changed thereafter (assuming no dynamic reclassification, which most languages forbid). A method call `obj.method(args)` compiles not to a direct call but to: load the vtable pointer from `obj`, index into a fixed slot, and call the function pointer found there, passing `obj` itself as an implicit first argument (`this`/`self`).
Building the vtable. When a subclass `B` extends `A`:
- Every virtual method inherited unchanged from `A` keeps its slot, pointing at `A`'s implementation.
- Every method `B` overrides replaces the pointer in that same slot with `B`'s new implementation.
- Every new virtual method `B` introduces gets a new slot appended at the end.
This slot-stability is exactly what makes dispatch O(1): the compiler statically determines which slot a call site targets (by the method's name and the static type's layout), and defers only which pointer occupies that slot to run time. ``` ALGORITHM: build_vtable(class C) if C has no superclass: table := empty list of (name -> slot index) slots := empty array of function pointers else: (table, slots) := build_vtable(C.superclass) # copy inherited layout table := copy(table); slots := copy(slots) for each method m declared in C: if m.name in table: # overriding an inherited method idx := table[m.name] slots[idx] := address_of(m.body) else: # brand-new virtual method idx := length(slots) table[m.name] := idx append slots with address_of(m.body) C.vtable := slots C.slot_of := table # compile-time map, not stored per-object return (table, slots)
ALGORITHM: dispatch(obj, method_name, args) cls := obj.dynamic_type # read the hidden class tag / vtable pointer idx := STATIC lookup of method_name's slot, resolved at compile time using the STATIC type of the call site fn := cls.vtable[idx] # ONE indirection, resolved at run time return fn(obj, args) # obj passed as implicit 'this' ``` The key subtlety, and the one students most often get wrong: the slot index is a compile-time constant determined by the static type at the call site (or, more precisely, by the method's declared signature within the class hierarchy). Only the contents of that slot — which function pointer actually lives there — depends on the object's dynamic type, discovered at run time via the object's stored vtable pointer. This split is exactly why dispatch costs one extra memory load and one indirect call, not a search: no scanning, no hashing, no `if`-chain.
Construction and single dispatch. During object construction, the vtable pointer is set to the currently constructing class before that class's constructor body runs, which is why calling an overridable method from within a base-class constructor famously does not yet see the derived class's override in languages like C++ (the vtable pointer is still `A`'s at that point) — a well-known trap discussed below. ```
```
## Complexity & correctness
Time. A dynamically dispatched call costs O(1): one load to fetch the object's vtable pointer, one array index (constant offset, known at compile time) to fetch the function pointer, and one indirect call. This is asymptotically identical to a direct call except for the extra indirection, typically 1–3 additional cycles on modern hardware absent a branch-prediction miss (indirect calls are harder for the CPU's branch predictor than direct calls, which is the practical, not asymptotic, cost of virtual dispatch). Contrast this with a naive re-implementation using a type tag and an `if`/`switch` chain — also O(1) amortized if the switch compiles to a jump table, but O(n) in the number of cases if it compiles to a linear chain of comparisons, and in either case it requires editing that chain for every new subtype, which is precisely the maintenance cost OOP eliminates.
Space. Each class stores one vtable of size O(m), where m is the number of virtual methods reachable in that class's hierarchy — a fixed, shared cost independent of the number of objects. Each object pays exactly one extra pointer (typically one machine word, e.g., 8 bytes on a 64-bit system) for its vtable pointer, regardless of how many methods the class declares. This is the crucial efficiency argument: vtable space is amortized across all instances, so creating a million `Circle` objects costs a million extra words, not a million copies of the vtable.
Why building the vtable is correct. The construction algorithm above maintains an invariant by structural induction on the inheritance chain: slot assignment is stable under inheritance — if method `m` occupies slot `k` in class `A`, then `m` occupies slot `k` in every subclass of `A`, whether inherited unchanged or overridden. Base case: a root class assigns slots in declaration order, trivially stable (there is nothing to be stable with). Inductive step: `build_vtable(B)` for `B extends A` starts by copying `A`'s table and slot array verbatim, so every method already present in `A` keeps its slot number; overriding only replaces the pointer at that slot, never renumbering it. New methods are appended after all inherited slots, so they cannot collide with or shift existing indices. This invariant is exactly what allows the compiler to resolve a call site's slot index using only the static type — the slot number is guaranteed to be the same no matter which subclass the object turns out to be at run time.
Why dispatch resolves to the correct override. Given the slot-stability invariant, dispatch correctness reduces to a simple claim: at the moment a method call executes, the object's stored vtable pointer refers to the vtable of its most specific (leaf) dynamic class, and by construction that vtable's slot `k` contains the most-derived override of the method assigned to slot `k`, because later `build_vtable` calls always overwrite earlier pointers when overriding occurs, never the reverse. Hence dispatch always yields the same result as literally asking "does the leaf class override this method? if not, does its parent? ..." walking up the chain — but computed in O(1) instead of O(depth of hierarchy).
Honest tradeoffs. Dynamic dispatch trades a small, constant runtime cost and one pointer of space per object for the ability to add new subclasses without modifying existing code (the open/closed principle: open for extension, closed for modification). It also trades readability of control flow — a call to `shape.draw()` can silently invoke any of dozens of overrides scattered across a codebase, unlike a direct call whose target is textually visible — for extensibility. Static dispatch remains strictly cheaper and more predictable when the set of cases is fixed and known, which is why languages let you opt out (`final` methods in Java, non-`virtual` methods in C++) to recover direct-call performance and eliminate the indirection entirely.
## Worked example Consider this small hierarchy (syntax kept language-neutral): ``` class Shape: method area() -> Number: return 0 method draw() -> String: return "generic shape" method name() -> String: return "Shape" class Circle extends Shape: field radius: Number method area() -> Number: return 3.14159 radius radius method draw() -> String: return "circle r=" + radius
# 'name' not overridden — inherited from Shape class Ellipse extends Circle: field minor_radius: Number method area() -> Number: return 3.14159 radius minor_radius
# 'draw' and 'name' not overridden — inherited (from Circle, Shape respectively) ```
Step 1 — vtable construction (compile time / class-load time).
| Class | Slot 0 (`area`) | Slot 1 (`draw`) | Slot 2 (`name`) |
|---|---|---|---|
| `Shape` | `Shape::area` | `Shape::draw` | `Shape::name` |
| `Circle` | `Circle::area` | `Circle::draw` | `Shape::name` (copied, unchanged) |
| `Ellipse` | `Ellipse::area` | `Circle::draw` (inherited, unchanged) | `Shape::name` (inherited, unchanged) |
Note the slot numbers never change — `area` is slot 0 in all three classes, exactly as the induction argument guarantees.
Step 2 — objects and their vtable pointers (run time). ``` s : Shape = new Shape() # s.vptr → Shape::vtable c : Shape = new Circle(radius=5) # c.vptr → Circle::vtable (declared type Shape!) e : Shape = new Ellipse(radius=5, minor_radius=2) # e.vptr → Ellipse::vtable ``` Note carefully: `c` and `e` are declared with static type `Shape` (perhaps returned from a factory function `makeRandomShape()`), but their dynamic type is `Circle` and `Ellipse` respectively.
Step 3 — dispatching a loop over a list of shapes. ``` shapes : List = [s, c, e] for sh in shapes: print(sh.area()) ``` The compiler resolves `sh.area()` to slot 0, a fixed decision made once, using only the static type `Shape`. At run time, for each iteration:
| Iteration | `sh` bound to | `sh.vptr` → | slot 0 contents | call result |
|---|---|---|---|---|
| 1 | `s` | `Shape::vtable` | `Shape::area` | `0` |
| 2 | `c` | `Circle::vtable` | `Circle::area` | `78.54` (using `radius=5`) |
| 3 | `e` | `Ellipse::vtable` | `Ellipse::area` | `31.42` (using `radius=5, minor=2`) |
Every call site is textually identical (`sh.area()`), every call resolves the same slot index (0), and yet three different function bodies execute — that is dynamic dispatch working exactly as designed. Now trace `sh.name()` for the same list: it resolves to slot 2 in all three cases, and because neither `Circle` nor `Ellipse` overrides `name`, every object's slot 2 still points at `Shape::name`, so all three calls print `"Shape"` — demonstrating that inheritance without overriding is simply "the pointer in that slot was never replaced," not a separate mechanism.
## Common pitfalls & traps
| Pitfall | Why it happens | Consequence |
|---|---|---|
| Calling a virtual method from a base-class constructor | Object's vtable pointer is still set to the base class while its constructor runs | Silently invokes the base version even if a subclass overrides it (C++); or invokes an override on a not-yet-initialized subclass state (Java/C#), causing null-field bugs |
| Confusing overriding with overloading | Overriding replaces a vtable slot (run-time choice); overloading picks among distinct methods by static argument types (compile-time choice) | Adding a method with a slightly different signature "to override" actually creates a new, unrelated overload — the old one is still callable |
| Slicing (in value-semantics languages like C++) | Assigning a derived object into a base-typed value (not pointer/reference) copies only the base portion, dropping the vtable pointer's derived identity | `Shape s = someCircle;` loses `Circle`'s behavior entirely; calls dispatch as plain `Shape` |
| Violating Liskov Substitution | Subclass changes a method's precondition/postcondition in a way that breaks client assumptions about the superclass's contract | Code that works for all `Shape`s crashes or misbehaves specifically for `Square`, breaking the "is-a" promise the type system does not check |
| Fragile base class problem | A base class change (e.g., altering a helper method's internal call order) silently changes behavior of every subclass that overrides a method it calls | Subclasses written and tested independently break after an unrelated base-class edit, with no compile error |
| Deep hierarchies for code reuse alone | Inheritance is used purely to avoid copy-pasting fields/methods, with no real is-a relationship | Produces brittle, hard-to-refactor hierarchies; "favor composition over inheritance" exists precisely to avoid this |
Pioneers & history
The object-oriented paradigm did not spring from a single insight; it accreted from a simulation language's practical needs, a research group's philosophical vision of "everything is a message," and later industrial languages that traded purity for performance and static type safety.
Origins · 1 minOle-Johan Dahl and how this was discovered
Dahl, together with Kristen Nygaard, designed Simula 67 to model discrete-event systems (queues, processes, resources), and in doing so introduced classes, objects, subclasses, and virtual procedures — the first working implementation of dynamic dispatch — as a byproduct of needing to simulate heterogeneous interacting entities cleanly. Their goal was simulation fidelity, not a general programming philosophy, yet the mechanism they built became the template every later object-oriented language would refine.
Where this matters
Virtual method tables and dynamic dispatch underpin nearly every mainstream object-oriented runtime: the JVM and CLR both implement `invokevirtual`/`callvirt` bytecode instructions using vtable-like structures (the JVM additionally uses inline caches to speed up repeated calls at the same call site). GUI frameworks (Java Swing, .NET WinForms, the browser DOM's event handlers) rely on overriding `paint`/`onEvent`-style methods so that a framework written once can render arbitrary custom widgets supplied later by application code — the direct descendant of the "add a new shape without touching drawing code" motivation from the overview. Plugin architectures (compiler backends targeting multiple instruction sets, database drivers implementing a common `Connection` interface, device drivers implementing a common `Driver` interface in an operating system) are essentially large-scale applications of dynamic dispatch: a fixed interface, an open set of implementations, resolved at run time by the loader. Understanding vtables also explains real compiler and JIT behavior: devirtualization (a compiler proving only one implementation is possible and replacing an indirect call with a direct one) is a major optimization in modern JITs precisely because it removes the indirection this article describes, and understanding Names, Binding, and Scope alongside dynamic dispatch clarifies why languages distinguish `virtual`/`override`/`final` keywords as binding-time annotations rather than mere style choices.
Ask the tutor: Can you trace what happens to the vtable and dispatch when a class overrides a method but also changes its parameter types — is that an override or a new overload?
Is dynamic dispatch the same thing as polymorphism?
Dynamic dispatch implements one important kind of polymorphism, called subtype polymorphism, but the two terms are not synonyms. Polymorphism broadly means "one interface, many implementations," and includes parametric polymorphism (generics/templates, resolved without any run-time type check) and ad-hoc polymorphism (overloading, resolved by static argument types at compile time). Dynamic dispatch is specifically the mechanism by which subtype polymorphism is realized at run time: a call written once against a supertype interface invokes different code depending on the object's actual dynamic type. So dynamic dispatch is a mechanism; polymorphism is the broader design property it serves, alongside two other mechanisms that need no run-time indirection at all.
Why does encapsulation matter if I could just document "please don't touch this field"?
Documentation is advisory; the compiler does not check it, and large codebases with many contributors will eventually violate an undocumented or lightly-documented convention. Encapsulation converts an informal convention into an enforced boundary: if a field is `private`, the compiler rejects any code outside the class that reads or writes it directly, full stop. This matters because the class's methods are the only code trusted to maintain the representation invariant (e.g., "the cached `size` field always matches the list's real length"); once outside code can bypass those methods, that invariant can be silently broken, and every method that relies on it becomes unreliable. Encapsulation is what allows a class's internal representation to change later without a compiler-verified guarantee that nothing external depended on it.
Why is single dispatch called "single" — is there such a thing as multiple dispatch?
Single dispatch means the method chosen depends on the dynamic type of exactly one object — the receiver (`this`/`self`) — while all other arguments are resolved using ordinary static overload resolution. This is what vtables directly support, since each object's vtable is indexed only by its own dynamic type. Multiple dispatch (found in languages like Common Lisp's CLOS, Julia, and Dylan) chooses a method based on the dynamic types of several arguments simultaneously — useful for binary operations like `collide(shapeA, shapeB)` where the correct implementation genuinely depends on both operands' runtime types. Multiple dispatch cannot be implemented with a simple one-dimensional vtable; it typically requires a multi-dimensional lookup table or a more elaborate method-resolution search, which is one reason it is rarer and costs more per call.
What exactly is the "fragile base class problem" and why can't the type system prevent it
It is the phenomenon where a seemingly safe, localized change to a base class — reordering internal method calls, changing a helper's return value, adjusting when a hook method fires — silently breaks subclasses that override methods and rely on undocumented assumptions about the base class's internal behavior. The type system cannot prevent this because type checking only verifies method signatures match (same name, parameter types, return type), never that the runtime behavior of an override remains compatible with what the base class's other methods implicitly assume about call order or side effects. This is why some languages (and most style guides) recommend documenting a class's "self-use" of its own methods explicitly, or preferring composition over deep inheritance to shrink the surface area where such implicit contracts can be violated.
Does every object-oriented language use vtables for dispatch?
No — vtables are the dominant strategy for statically-typed, single-inheritance-friendly languages (C++, Java, C#) because slot numbers can be fixed at compile time. Dynamically typed languages like Python, Ruby, and Smalltalk instead resolve method calls via a method lookup along the class chain at run time, often cached in a hash table keyed by method name, because the set of methods an object responds to can change dynamically (methods can be added to a class at run time) in ways a fixed-slot vtable cannot accommodate cleanly. Languages supporting multiple inheritance of implementation (not just interfaces) also complicate simple vtables, requiring more elaborate layouts (e.g., C++'s multiple vtable pointers for multiply-inherited classes) or entirely different resolution schemes.
If dynamic dispatch has overhead, why not just always use static dispatch
Static dispatch is strictly faster and more predictable, and languages let you choose it explicitly (`final` methods, non-virtual functions, sealed classes) precisely because it is preferable whenever the set of possible implementations is fixed and known at compile time. The reason not to use it everywhere is that many real systems need to accept behavior that does not exist yet at compile time: a plugin loaded later, a subclass written by a different team, a widget type invented after the framework shipped. Static dispatch cannot express "call whatever `draw` implementation this future, unknown object happens to have," because it requires the compiler to know the target address in advance. Dynamic dispatch trades a small, bounded per-call cost for this open-ended extensibility, which is a good trade whenever extensibility is actually needed.
How does the compiler know the slot index if it doesn't know the object's real class?
It relies entirely on the static type of the expression at the call site — the declared type of the variable, parameter, or return value, as established at compile time by scope and declaration rules (the same machinery covered under Names, Binding, and Scope). Because the vtable-construction algorithm guarantees every subclass preserves the slot numbering of its ancestors, the compiler only needs to know "this expression has static type `Shape`, and `Shape` declares `draw` at slot 1" — it never needs to know which concrete subclass will actually be there at run time. That is the entire point of the slot-stability invariant: it decouples the compile-time question (which slot?) from the run-time question (whose function pointer is in that slot?).