SovranCode
Modern C++ Lesson 40
Course contents50 available · 0 planned

Get a program running

01 · C++ Introduction: What Is C++?02 · How to Install a C++ Compiler and Set Up Your Editor03 · C++ Hello World: Your First Program Explained04 · How to Compile and Run C++ Programs05 · C++ Debugging: Warnings and Breakpoints

Work with values and input

06 · C++ Variables: Initialization and Scope07 · C++ Data Types: int, double and Type Conversion08 · C++ const, constexpr and auto Explained09 · C++ Operators: Arithmetic, Comparison and Logic10 · C++ User Input: cin, getline and Validation

Control flow and functions

11 · C++ Conditions: if, else if and else12 · C++ Switch Statement: Cases, break and default13 · C++ For Loop: Syntax and Examples14 · C++ While and Do-While Loops: Examples15 · C++ Functions: Parameters and Return Values

Strings and sequences

16 · C++ Strings: std::string with Examples17 · C++ Vector: std::vector with Examples18 · C++ Arrays: std::array and Bounds Checking19 · C++ Range-Based For Loop and Iterators20 · C++ Vector Practice: Build a Data Summary

Model data with classes

21 · C++ Structs and enum class Explained22 · C++ Classes and Objects: Encapsulation23 · C++ Constructors and Member Initialization24 · C++ Const Member Functions and Composition25 · C++ Copying Objects and the Rule of Zero

Lifetime and resource ownership

26 · C++ Object Lifetime, Destructors and RAII27 · C++ Pointers and References: nullptr and Borrowing28 · C++ unique_ptr: Smart Pointers and Ownership29 · C++ Move Semantics: std::move vs Copying30 · C++ string_view and span: Avoid Dangling Views

Containers and algorithms

31 · C++ map vs unordered_map: Lookup and Examples32 · C++ set, queue and stack: When to Use Each33 · C++ Lambda Expressions and STL Algorithms34 · C++ sort and binary_search: Rules and Complexity35 · C++20 Ranges: Views, filter and transform

Errors, files, and interfaces

36 · C++ optional and variant Explained37 · C++ Exceptions: try, catch and Safe Cleanup38 · C++ File Handling: Read and Write Text Files39 · C++ Input Parsing and Data Validation40 · C++ Polymorphism: Virtual Functions and Interfaces

Generic programming and quality

41 · C++ Templates: Function and Class Examples42 · C++20 Concepts and Template Constraints43 · C++ Unit Testing: Normal and Boundary Cases44 · C++ Sanitizers: Find Memory and Undefined Behavior Bugs45 · C++ Performance: Benchmark Before Optimizing

Build and deliver a complete application

46 · C++ Header Files and Translation Units47 · C++ CMake Tutorial: Build an Executable48 · C++ Dependencies and CTest: Automated Tests49 · C++ Multithreading: Threads and Data Races50 · C++ Command-Line Applications: Help and Release Checks
Modern C++50 available · 0 planned

Get a program running

01 · C++ Introduction: What Is C++?02 · How to Install a C++ Compiler and Set Up Your Editor03 · C++ Hello World: Your First Program Explained04 · How to Compile and Run C++ Programs05 · C++ Debugging: Warnings and Breakpoints

Work with values and input

06 · C++ Variables: Initialization and Scope07 · C++ Data Types: int, double and Type Conversion08 · C++ const, constexpr and auto Explained09 · C++ Operators: Arithmetic, Comparison and Logic10 · C++ User Input: cin, getline and Validation

Control flow and functions

11 · C++ Conditions: if, else if and else12 · C++ Switch Statement: Cases, break and default13 · C++ For Loop: Syntax and Examples14 · C++ While and Do-While Loops: Examples15 · C++ Functions: Parameters and Return Values

Strings and sequences

16 · C++ Strings: std::string with Examples17 · C++ Vector: std::vector with Examples18 · C++ Arrays: std::array and Bounds Checking19 · C++ Range-Based For Loop and Iterators20 · C++ Vector Practice: Build a Data Summary

Model data with classes

21 · C++ Structs and enum class Explained22 · C++ Classes and Objects: Encapsulation23 · C++ Constructors and Member Initialization24 · C++ Const Member Functions and Composition25 · C++ Copying Objects and the Rule of Zero

Lifetime and resource ownership

26 · C++ Object Lifetime, Destructors and RAII27 · C++ Pointers and References: nullptr and Borrowing28 · C++ unique_ptr: Smart Pointers and Ownership29 · C++ Move Semantics: std::move vs Copying30 · C++ string_view and span: Avoid Dangling Views

Containers and algorithms

31 · C++ map vs unordered_map: Lookup and Examples32 · C++ set, queue and stack: When to Use Each33 · C++ Lambda Expressions and STL Algorithms34 · C++ sort and binary_search: Rules and Complexity35 · C++20 Ranges: Views, filter and transform

Errors, files, and interfaces

36 · C++ optional and variant Explained37 · C++ Exceptions: try, catch and Safe Cleanup38 · C++ File Handling: Read and Write Text Files39 · C++ Input Parsing and Data Validation40 · C++ Polymorphism: Virtual Functions and Interfaces

Generic programming and quality

41 · C++ Templates: Function and Class Examples42 · C++20 Concepts and Template Constraints43 · C++ Unit Testing: Normal and Boundary Cases44 · C++ Sanitizers: Find Memory and Undefined Behavior Bugs45 · C++ Performance: Benchmark Before Optimizing

Build and deliver a complete application

46 · C++ Header Files and Translation Units47 · C++ CMake Tutorial: Build an Executable48 · C++ Dependencies and CTest: Automated Tests49 · C++ Multithreading: Threads and Data Races50 · C++ Command-Line Applications: Help and Release Checks
PREVIOUS LESSONC++ Input Parsing and Data Validation
NEXT LESSONC++ Templates: Function and Class Examples
C++ foundations · Lesson 40 About 50 min with practice

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.

Before you begin

Know classes, const member functions, references, composition and unique_ptr. Inheritance is introduced only for objects that need the same public operation with different behavior.

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.

One format-text interface can refer to a plain formatter returning hello or a bracket formatter returning bracketed hello. The concrete objects implement the same operation. A separate report object owns a formatter helper to illustrate composition rather than copying a base object.
A base reference can call the concrete object's virtual implementation. Composition instead means one object has a helper; use the simpler relationship when runtime substitution is not needed.
C++ PRACTICE RUNNER

Call two implementations through one base reference

The reference does not copy the concrete object. Each override supplies the same const operation.

#include <iostream>
#include <string>
struct Formatter {
    virtual std::string format(const std::string& text) const = 0;
    virtual ~Formatter() = default;
};
struct Plain : Formatter {
    std::string format(const std::string& text) const override { return text; }
};
struct Bracket : Formatter {
    std::string format(const std::string& text) const override { return "[" + text + "]"; }
};
void show(const Formatter& formatter) {
    std::cout << formatter.format("hello") << '\n';
}
int main() {
    Plain plain;
    Bracket bracket;
    show(plain);
    show(bracket);
}
TERMINAL OUTPUT
Predict the output, edit the source, then select Run code.

C++20 compiler runner: source and input are sent only when you press Run to this website's execution endpoint, not an external vendor. Local preview requires Docker and gcc:14; production requires a separately isolated execution service. Network is disabled; execution is limited to 3 seconds, compilation to 10 seconds, memory to 384 MB and output to 32 KB. Single-file programs only. Don't enter secrets. Edits reset on refresh; Copy your work to keep it.

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.
C++ PRACTICE RUNNER

Repair a mismatched override

Intentional compile error: add const before override so the function matches the interface exactly.

#include <string>
struct Formatter {
    virtual std::string format(const std::string& text) const = 0;
    virtual ~Formatter() = default;
};
struct Plain : Formatter {
    std::string format(const std::string& text) override { return text; }
};
int main() { Plain plain; }

TERMINAL OUTPUT
Predict the output, edit the source, then select Run code.

C++20 compiler runner: source and input are sent only when you press Run to this website's execution endpoint, not an external vendor. Local preview requires Docker and gcc:14; production requires a separately isolated execution service. Network is disabled; execution is limited to 3 seconds, compilation to 10 seconds, memory to 384 MB and output to 32 KB. Single-file programs only. Don't enter secrets. Edits reset on refresh; Copy your work to keep it.

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.

C++ PRACTICE RUNNER

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.

#include <iostream>
struct Base {
    virtual const char* name() const { return "Base"; }
    virtual ~Base() = default;
};
struct Child : Base {
    const char* name() const override { return "Child"; }
};
int main() {
    Child child;
    Base copy = child;
    const Base& reference = child;
    std::cout << copy.name() << '\n';
    std::cout << reference.name() << '\n';
}
TERMINAL OUTPUT
Predict the output, edit the source, then select Run code.

C++20 compiler runner: source and input are sent only when you press Run to this website's execution endpoint, not an external vendor. Local preview requires Docker and gcc:14; production requires a separately isolated execution service. Network is disabled; execution is limited to 3 seconds, compilation to 10 seconds, memory to 384 MB and output to 32 KB. Single-file programs only. Don't enter secrets. Edits reset on refresh; Copy your work to keep it.

Compare with the starter's expected output
Base
Child

After editing inputs, calculate a new prediction; this reference describes only the starter.

C++ PRACTICE RUNNER

Use a contained helper when substitution is unnecessary

Report has a PrefixFormatter. It is not a kind of PrefixFormatter and needs no virtual function.

#include <iostream>
#include <string>
class PrefixFormatter {
public:
    std::string format(const std::string& text) const { return "Note: " + text; }
};
class Report {
    PrefixFormatter formatter_;
public:
    std::string line(const std::string& text) const { return formatter_.format(text); }
};
int main() {
    const Report report;
    std::cout << report.line("hello") << '\n';
}
TERMINAL OUTPUT
Predict the output, edit the source, then select Run code.

C++20 compiler runner: source and input are sent only when you press Run to this website's execution endpoint, not an external vendor. Local preview requires Docker and gcc:14; production requires a separately isolated execution service. Network is disabled; execution is limited to 3 seconds, compilation to 10 seconds, memory to 384 MB and output to 32 KB. Single-file programs only. Don't enter secrets. Edits reset on refresh; Copy your work to keep it.

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.

LOCAL TERMINAL COMMANDS

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.exe
APPLY THE CONCEPT

Independent 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?
  1. The mode line is exact text; only the separate count reader accepts surrounding numeric whitespace.
  2. Use make_unique for the selected class, then borrow *formatter for reporting.
  3. Return a string value from each format operation and leave the words unchanged.
C++ PRACTICE RUNNER

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.

#include <iostream>
#include <string>
#include <sstream>
#include <limits>

int main() {
    // Implement the acceptance checklist above.
    return 0;
}

TERMINAL OUTPUT
Predict the output, edit the source, then select Run code.

C++20 compiler runner: source and input are sent only when you press Run to this website's execution endpoint, not an external vendor. Local preview requires Docker and gcc:14; production requires a separately isolated execution service. Network is disabled; execution is limited to 3 seconds, compilation to 10 seconds, memory to 384 MB and output to 32 KB. Single-file programs only. Don't enter secrets. Edits reset on refresh; Copy your work to keep it.

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
C++ PRACTICE RUNNER

Reference lab solution

Compare the reference with your own reasoning. Running this solution does not grade or complete the lesson.

#include <iostream>
#include <sstream>
#include <string>
bool readInteger(int& value) {
    std::string line;
    if (!std::getline(std::cin, line)) return false;
    std::istringstream parser{line};
    if (!(parser >> value)) return false;
    parser >> std::ws;
    return parser.eof();
}
#include <memory>
#include <vector>
struct Formatter {
    virtual std::string format(const std::string& text) const = 0;
    virtual ~Formatter() = default;
};
struct Plain : Formatter {
    std::string format(const std::string& text) const override { return text; }
};
struct Bracket : Formatter {
    std::string format(const std::string& text) const override { return "[" + text + "]"; }
};
bool validWord(const std::string& word) {
    if (word.empty() || word.size() > 12) return false;
    const std::string letters{"abcdefghijklmnopqrstuvwxyz"};
    for (char ch : word) if (letters.find(ch) == std::string::npos) return false;
    return true;
}
void report(const Formatter& formatter, const std::vector<std::string>& words) {
    for (const auto& word : words) std::cout << formatter.format(word) << '\n';
    std::cout << "Count: " << words.size() << '\n';
}
int main() {
    std::string mode;
    int count{};
    if (!std::getline(std::cin, mode) || (mode != "plain" && mode != "bracket")
        || !readInteger(count) || count < 0 || count > 4) {
        std::cout << "Invalid input\n"; return 1;
    }
    std::vector<std::string> words;
    for (int i = 0; i < count; ++i) {
        std::string word;
        if (!std::getline(std::cin, word) || !validWord(word)) {
            std::cout << "Invalid input\n"; return 1;
        }
        words.push_back(word);
    }
    std::unique_ptr<Formatter> formatter;
    if (mode == "plain") formatter = std::make_unique<Plain>();
    else formatter = std::make_unique<Bracket>();
    report(*formatter, words);
}
TERMINAL OUTPUT
Predict the output, edit the source, then select Run code.

C++20 compiler runner: source and input are sent only when you press Run to this website's execution endpoint, not an external vendor. Local preview requires Docker and gcc:14; production requires a separately isolated execution service. Network is disabled; execution is limited to 3 seconds, compilation to 10 seconds, memory to 384 MB and output to 32 KB. Single-file programs only. Don't enter secrets. Edits reset on refresh; Copy your work to keep it.

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.

  • C++ draft: virtual functions
  • C++ draft: abstract classes
  • C++ Core Guidelines: class hierarchies
KNOWLEDGE CHECK

Check your understanding

Answer all five questions, then review the explanations. Answers are not saved or synced; completion is a separate self-review choice.

01Why does show accept const Formatter&?
02What does override help check?
03Why is the interface destructor virtual here?
04What is object slicing?
05When is composition a good alternative?
PREVIOUS LESSONC++ Input Parsing and Data Validation
NEXT LESSONC++ Templates: Function and Class Examples
ON THIS PAGELearning objectivesOne operation can have different implementationsThe reference keeps the real object involvedA base value copy is not a base referenceHas-a is often simpler than is-aSelect a formatter and keep ownership clearIndependent labReviewKnowledge checkReferences
Course overview