The virtual keyword is the single switch that turns a C++ member function into something the runtime can dispatch on. Without it, a call through a base pointer or reference picks the implementation at compile time and runs the base version. With it, the same call defers the decision to runtime and runs whichever version belongs to the actual object. This lesson digs into what virtual does to a function, how to call the base version from inside an override, and two corners of the language where virtual dispatch behaves in ways that surprise people.
virtual Actually ChangesStripped to one sentence: virtual changes a member function from being statically resolved (compiler picks based on the pointer/reference type) to being dynamically dispatched (runtime picks based on the actual object).
The keyword goes in the base class, before the return type, on the method's declaration. Once the base function is virtual, the same function in every derived class is virtual too, automatically. This is a small detail worth pinning down because it affects how you write derived classes.
The call p->applyDiscount() goes through a Product*, but it lands in DigitalProduct::applyDiscount because Product::applyDiscount is virtual. The price drops by 30%, not 10%.
The rule is: virtual belongs on the base method's declaration, and the only thing it changes about that method is the dispatch strategy. The function body, the parameter list, the return type, everything else stays the same.
A wrinkle compared to other languages: in C++, virtualness is inherited. If a method is virtual in the base, it is virtual in every derived class that overrides it, whether the virtual keyword appears on the derived declaration or not.
Product::describe is the only declaration that says virtual, but DigitalProduct::describe and EbookProduct::describe are virtual all the way down. The call through Product* correctly dispatches to EbookProduct::describe, three levels deep.
So why isn't virtual written on the derived declarations? Two reasons. First, it's redundant: the language already knows the function is virtual, so repeating the keyword adds nothing. Second, the modern C++ convention is to write override on the derived declaration instead. override was added in C++11 specifically to mark intent and let the compiler verify that the derived function actually overrides a virtual base function. The rule of thumb: "write virtual in the base, write override in every derived class".
Writing virtual void describe() const override; in a derived class is legal. It compiles. It carries two keywords doing one job's worth of work. Pick override.
A common pattern: the derived override wants to do everything the base does, plus a little extra on top. The natural-looking solution is to call the same-named function from inside the override. That breaks badly. The unqualified call resolves to the override itself, and the function recurses into itself until the stack runs out.
The correct way is to qualify the call with the base class name: Base::method(). The qualified call bypasses virtual dispatch and invokes the named version directly.
E-commerce example. Product::applyDiscount takes 10% off any product. DigitalProduct overrides it to take the base 10% first, then knocks off another 20% because digital goods cost almost nothing to deliver.
The math: \$100 multiplied by 0.90 gives \$90, then 0.80 of that gives \$72. The two discounts stack because the override delegates to the base first and then adds its own logic. Writing applyDiscount() instead of Product::applyDiscount() would have called DigitalProduct::applyDiscount again, which would call itself, and so on. A reliable way to crash the program in one line.
The Base::method() syntax is the only way to invoke a base implementation that has been overridden in the current class. It works for any level of the hierarchy too. If EbookProduct extends DigitalProduct and wants the digital discount on top of the bundle bonus, it writes DigitalProduct::applyDiscount(). Naming the class selects exactly that version.
A qualified Base::method() call is a direct (non-virtual) call. There is no vtable lookup, even when the function is virtual. The compiler emits the same code it would for a regular static method call.
Sometimes the base class shouldn't have an implementation. Product::applyDiscount taking a generic 10% off works as a default, but in some hierarchies the base has no sensible default at all. What should Shape::area return for a "shape" that has no actual geometry? What should PaymentMethod::charge do when "payment method" is an abstract idea, not a concrete way to pay?
C++ has a syntax for this: declare the function virtual and assign it to zero. That makes it a pure virtual function, and the class that contains it is automatically an abstract class that cannot be instantiated directly.
The = 0 at the end of the declaration is the syntax. It means "this function has no body in this class, and any concrete derived class must provide one". Trying to instantiate PaymentMethod directly produces a compile error. Pure virtual functions are how C++ expresses interfaces: the base says "subclasses must implement this", the derived class delivers, and code that talks to the base type gets polymorphic behavior automatically.
For now, the takeaway is the syntax and the idea: = 0 makes a function abstract, and the class becomes abstract along with it.
The kind of bug that survives code review because the code looks right, the behavior looks plausible, and the actual issue only matters in one specific call shape. The rule is simple but counterintuitive:
Default arguments are resolved at compile time using the static type. The function body is resolved at runtime using the dynamic type.
Read that again. Two halves of the same call get resolved using two different types. When the defaults differ between base and derived, the result is a function call that picks one set of arguments and one body, where each comes from a different class. Output: confusion.
Two surprises in three lines of output. The function that ran was the derived DigitalProduct::applyDiscount (you can see "Digital:" in the print), but the default value for percent was 10.0, which is the base's default, not the derived's 30.0. That's why the price dropped by only 10% even though the digital override ran.
What happened: the compiler looked at the call p->applyDiscount() and saw p had static type Product*. It filled in the missing argument from Product::applyDiscount's default (percent = 10.0). Then it emitted a virtual call. At runtime, the vtable lookup found DigitalProduct::applyDiscount and ran that, but the argument was already baked in as 10.0 from the compile-time decision.
The simplest fix is the one most teams use: don't give virtual functions default arguments at all. If the base has no defaults and the derived has no defaults, there is nothing to mismatch. Callers always pass the argument explicitly, and the question of "which default wins" never comes up.
For a "default discount" concept, express it without leaning on language-level defaults. Two clean options: provide a non-virtual overload that calls the virtual one with a sensible value, or expose a separate function with an obvious name.
Now the "default" is part of the function name, not buried in the parameter list. There is no signature mismatch to argue about, and the dispatch is unambiguous.
Inside a base class's constructor or destructor, virtual calls do not dispatch to the derived override. They dispatch to the base's own version. This is by design, not a bug, and the reason is a memory-safety guarantee that costs you a feature you may not have wanted in the first place.
The rule, stated precisely: during construction and destruction of an object, the dynamic type of the object is the type of the constructor or destructor currently running. While Product's constructor runs, the object is a Product. The DigitalProduct parts haven't been initialized yet (or, during destruction, have already been torn down), so dispatching to DigitalProduct::applyDiscount would touch members that don't exist yet.
Concrete example. Product calls a virtual describe() from its constructor, expecting derived classes to plug in their own version.
Both the constructor and destructor lines say "I am a Product". The author of DigitalProduct probably expected "I am a DigitalProduct" both times, since the object is a DigitalProduct and describe is virtual. But during Product's constructor, the derived parts haven't been built yet, so the language wires the virtual call to Product::describe. During Product's destructor, the derived parts have already been torn down, so the same rule applies in reverse.
Why does the language work this way? Consider the alternative. If Product's constructor called DigitalProduct::describe, and DigitalProduct::describe tried to read a member that lives in DigitalProduct itself, it would be reading uninitialized memory. The language refuses to let that happen, even at the cost of a feature that would otherwise seem natural. The same logic applies in reverse for destructors: by the time Product's destructor runs, anything that was specific to DigitalProduct has already been destroyed.
The practical consequence: do not call virtual functions from constructors or destructors expecting derived behavior. For that pattern, two reliable alternatives exist.
Option 1: Two-step construction. Build the object, then call an initializer that runs after construction completes. The initializer is a normal member function, called on a fully constructed object, and virtual dispatch works as expected.
Option 2: Pass the behavior in. Hand the constructor a function object or a callback that contains the per-type behavior, sidestepping virtual dispatch entirely. This is heavier machinery and is overkill for most situations, but it's a clean option when the construction-time customization point genuinely matters.
Virtual functions are not free, but they are cheap, and the cost is well-understood. A virtual call adds one level of indirection compared to a direct call. The generated code reads a hidden pointer from the object (the vptr) to find the class's table of function pointers (the vtable), then reads the right entry from that table to find the actual function to call.
In a non-virtual call:
In a virtual call:
On modern CPUs, this is a few extra instructions and one or two extra memory accesses. The accesses usually hit L1 cache because the vtable is small and frequently used. In tight loops where the same function is called millions of times, the cost can show up in benchmarks. In normal application code, it's invisible.
Three things worth knowing about the cost.
| Concern | Reality |
|---|---|
| Per-call overhead | A few extra cycles, usually negligible |
| Memory per object | One extra pointer (the vptr) per polymorphic instance |
| Inlining | Virtual calls usually can't be inlined, because the compiler doesn't know which function will run |
The third item matters more than the first two for hot code. Inlining is one of the compiler's most effective optimizations, and it depends on knowing the function at compile time. A virtual call hides that knowledge. Some modern compilers can "devirtualize" a call when they can prove the dynamic type (a local object whose type never changes, for example), and then inline normally, but this is best-effort.
For a tight inner loop where the same virtual function is called billions of times on objects whose type is known statically, bypassing virtual dispatch with a templated approach (CRTP) sometimes pays off. For everyday code, write virtual and move on.
The model to remember: one hidden pointer per object, one table per class, one extra hop per call.
A small e-commerce hierarchy that uses everything in this lesson. Product is the base with a virtual applyDiscount and a virtual describe. DigitalProduct calls the base discount and adds an extra cut on top. SubscriptionProduct overrides both. A loop iterates over a vector of Product* and processes each item without knowing the concrete type.
The loop only knows about Product*. Three different discount rules, three different description formats, no if-chains. Each object's vptr points to the right vtable, the runtime picks the right function, and polymorphism works for the price of one extra indirection per call.
Two specific points in this example. First, DigitalProduct::applyDiscount uses Product::applyDiscount() to delegate the baseline discount to the base, then adds its own cut. No infinite recursion because of the qualifier. Second, no virtual function has default arguments, which sidesteps the pitfall entirely. There are no virtual calls in the constructors or destructors either, which sidesteps the other pitfall.
10 quizzes