C++ Polymorphism: Virtual Functions and Interfaces
Learn C++ virtual functions, override and interfaces with simple formatter examples, safe base references, virtual destruction and a clear comparison with composition.
What you will learn
- Explain a small interface as a shared set of operations.
- Call virtual behavior through a base reference.
- Use override to catch a signature mistake.
- Avoid object slicing and unsafe base destruction.
- Compare inheritance with a simpler composed helper.
One operation can have different implementations
Suppose a report needs to format a short word. One version returns hello unchanged. Another version returns [hello] with brackets. The report should not need to know every detail of those two implementations. A small interface can say that each formatter must provide format(text). The report works with that operation, while the selected object decides how the formatted string is produced.
An interface is a public agreement about operations. In C++, an abstract base class can express one. A pure virtual function ends with = 0, which means a concrete derived class must provide the required implementation before it can be instantiated. Abstract does not mean vague behavior: the operation still needs a documented meaning, input rules and result. Our example has one read-only operation returning a string value.
Polymorphism means using the same interface with different concrete behavior. You do not need a large class hierarchy to understand it. We use a base reference so the original concrete object is still there when the call happens. An ordinary if statement or a composed helper can be simpler when there are only a few fixed choices. Start with the actual need for substitution, not with a desire to make every class inherit from something.

Call two implementations through one base reference
The reference does not copy the concrete object. Each override supplies the same const operation.
Predict the output, edit the source, then select Run code.
Compare with the starter's expected output
hello
[hello]
After editing inputs, calculate a new prediction; this reference describes only the starter.
The reference keeps the real object involved
The first program creates a Plain object and a Bracket object. show accepts const Formatter&, so it can borrow either object through the shared interface. Calling format is virtual, meaning the implementation is selected according to the concrete object being used. The first call returns hello and the second returns [hello]. show does not need a type-name comparison or a separate branch for every derived class.
const after the member function means the operation is callable on a const formatter and does not change its ordinary members. override asks the compiler to check that the declaration really matches a virtual function in the base. It is not just a comment. If you accidentally omit const or change a parameter type, the compiler can point out that your intended override does not match the required operation.
The base destructor is virtual because this interface is intended to support owning polymorphic objects. If an owning pointer destroys a derived object through this base type, the appropriate destructor chain must run. unique_ptr<Base> by itself cannot repair a missing virtual destructor in such a design. The defaulted destructor here lets member objects handle their own cleanup and avoids teaching manual delete as an everyday solution.
- Create Plain and Bracket as separate concrete objects.
- show borrows either one as const Formatter&.
- The virtual call selects that object's matching override.
- The returned string is printed; neither formatter is copied or modified.
Repair a mismatched override
Intentional compile error: add const before override so the function matches the interface exactly.
Predict the output, edit the source, then select Run code.
A base value copy is not a base reference
Object slicing means copying only the base part of a derived object into a separate base object. The new value is a base object, not a disguised complete copy of the derived one. In the next example the base is concrete so that the copy is allowed. The copied object says Base, while a reference to the original derived object says Child. The difference comes from which object actually exists at the call.
An abstract base such as Formatter cannot be copied into a standalone Formatter value at all. That compile-time restriction helps, but it is not a replacement for understanding the relationship. A function taking a concrete base by value can still slice a derived argument. Use a reference or suitable pointer when the purpose is to call the original object's virtual behavior rather than make an independent base value.
A reference is borrowed access. The object it refers to must remain alive during the call. Do not return a base reference to a local derived object that will be destroyed as the function ends. If a selected implementation must live longer, give it an explicit owner. Our lab uses unique_ptr<Formatter> and a virtual destructor, keeping ownership and borrowing visible instead of leaving a raw pointer with an unclear lifetime.
Compare a base copy with a base reference
This safe example deliberately copies a concrete base part. It does not perform invalid casts or unsafe destruction.
Predict the output, edit the source, then select Run code.
Compare with the starter's expected output
Base
Child
After editing inputs, calculate a new prediction; this reference describes only the starter.
Use a contained helper when substitution is unnecessary
Report has a PrefixFormatter. It is not a kind of PrefixFormatter and needs no virtual function.
Predict the output, edit the source, then select Run code.
Compare with the starter's expected output
Note: hello
After editing inputs, calculate a new prediction; this reference describes only the starter.
Has-a is often simpler than is-a
Composition means that one object contains or uses another as a helper. The Report example has a PrefixFormatter member. Report is not claiming to be a formatter; it delegates one piece of work to the contained helper. This is often the right design when behavior is fixed and there is no need to substitute different implementations through one base type at runtime. No virtual table knowledge is needed to use this plan.
Inheritance should represent a useful public substitution relationship. If a derived class cannot honor the operation's meaning, sharing a method name is not enough. Keep the interface small and write what the caller may rely on. A formatter returning a string should not unexpectedly delete files or mutate unrelated application state. A clean signature helps, but the implementation must still respect the documented behavior.
Do not introduce dynamic allocation just to make a virtual call. The first example uses ordinary local objects and references. The lab uses unique_ptr to practise selecting and owning an implementation, but a small fixed application could use a simple conditional or another value-based design. The goal is to understand both options and choose deliberately. More inheritance layers do not automatically make a program more flexible or easier for students to maintain.
Select a formatter and keep ownership clear
Read an exact mode line, plain or bracket, then a whole-line count from zero through four, then that many words. Each word must contain one through twelve lowercase English letters, with no trimming or other characters. Validate the whole request before printing formatted words. Any invalid or missing required input prints Invalid input and returns status one. Empty data is valid when the mode and count were supplied correctly.
Create a unique_ptr<Formatter> holding the selected concrete class. Pass the formatter by const reference to a function that prints one formatted word per line, then print Count. Keep format const, use override and provide the virtual base destructor. Test both modes, zero words, repeated words, maximum word length, a mistyped mode and an invalid last word. Explain why the pointer owns the implementation while the reporting function only borrows it.
Compile and check one complete program
Save one example as main.cpp. Choose your installed compiler. Run only after a successful build. The in-page editor needs the configured compiler sandbox; these terminal commands are an alternative, not proof that the sandbox is running.
macOS / Linux:
clang++ -std=c++20 -Wall -Wextra -Wpedantic main.cpp -o lesson
# Or use g++ with the same options.
./lesson
Windows Developer Command Prompt:
cl /std:c++20 /EHsc /W4 main.cpp /Fe:lesson.exe
lesson.exeIndependent lab
Select one owned formatter and call its behavior through a small borrowed interface.
Acceptance checklist
- Read exact mode plain or bracket, count 0..4, then 1..12-letter lowercase-English words on separate lines.
- Validate all required input before output; reject with Invalid input and status 1.
- Use an abstract Formatter with const format and a virtual defaulted destructor.
- Implement Plain and Bracket with override; own the selected object with unique_ptr.
- Pass the interface by const reference, print each result and Count, and handle an empty group without a dereference of data elements.
Need a hint?
- The mode line is exact text; only the separate count reader accepts surrounding numeric whitespace.
- Use make_unique for the selected class, then borrow *formatter for reporting.
- Return a string value from each format operation and leave the words unchanged.
Write and run your lab
Build your own solution before revealing the reference. Use the input field for normal, boundary and invalid cases; this runner executes your program but does not grade it.
Predict the output, edit the source, then select Run code.
Your lab evidence and notes
Record predictions, actual output, commands and the cause of each repair. Code is plain text: this scratchpad does not execute or grade C++.
Loading local notes…
Review your acceptance checklist and quiz explanations before marking this lesson complete. This is self-review, not a grade or certification.
Reveal the explained lab solution
Reference lab solution
Compare the reference with your own reasoning. Running this solution does not grade or complete the lesson.
Predict the output, edit the source, then select Run code.
Expected output
[hello]
[cpp]
Count: 2
Validation is completed before any formatted word is printed, so an invalid last word cannot create a partial success report.
The selected concrete object is owned by unique_ptr and destroyed through a virtual base destructor. The report function borrows it without copying or slicing.
Each const override implements the same operation. Composition or a conditional could be simpler for a fixed two-mode product; the lab isolates runtime substitution for practice.
Review before moving on
Explain the lesson's objectives in your own words, compare your lab evidence with the acceptance checklist, and keep the first failing case alongside your repair. Completing a page is not proof that every C++ feature or input is supported.
- Explain a small interface as a shared set of operations.
- Call virtual behavior through a base reference.
- Use override to catch a signature mistake.
- Avoid object slicing and unsafe base destruction.
- Compare inheritance with a simpler composed helper.
References and further reading
Original examples and exercises accompany these primary references. Tool-specific diagnostics and support depend on your installed version.