Introduction

Runtime Polymorphism is lesson 107 in the C++ Beginner pathway. Runtime Polymorphism examines object invariants, special member functions, lookup, visibility, substitution, dynamic dispatch, lifecycle and extensibility.

This lesson develops a precise mental model, executable C++ examples, boundary and security analysis, verification guidance, practice questions and a hands-on project. Record the language mode, compiler, standard library, flags, target, dependencies and operating system used for your work.

Explanation

Learning outcomes

After completing Runtime Polymorphism, you should be able to explain the concept, identify the governing C++ language, standard-library, ABI, operating-system or hardware contract, predict a small example, implement a defensible solution and diagnose common failures. You should state accepted inputs, returned values, effects, exceptions, ownership, lifetime, cleanup, compatibility and security boundaries.

At the Beginner level, competence means more than recalling syntax or an API spelling. Connect Runtime Polymorphism to C++ types, values, objects, expressions, translation units, calls, resources and executable behavior. Advanced and research work also needs concurrency, performance, binary compatibility, hardware interaction and reproducibility arguments. One successful run proves one case; dependable software needs a contract and diverse evidence.

Core idea and scope

Runtime Polymorphism examines object invariants, special member functions, lookup, visibility, substitution, dynamic dispatch, lifecycle and extensibility. Beginner C++ connects source text, types, values, objects, expressions and control flow to compilation, linking and execution. Correct code makes ownership, lifetime, bounds and failure visible. Examples use portable modern C++ and state the required language mode. Record compiler, standard-library implementation, flags, target and operating system.

The core vocabulary includes translation unit, type, value category, object lifetime, invariant, RAII. Define each term in a concrete program. The ISO C++ standard and working drafts specify language and library behavior, while compilers, standard-library implementations, ABIs, operating systems, processors and third-party packages define additional contracts. Do not promote one observation from a development machine into a universal guarantee.

Scope the topic with four questions. What preconditions and input forms are accepted? What value, state change, I/O, message, response or artifact is produced? What can fail and how is failure represented? What invariant must hold before and after the operation? These questions convert a loose feature into a reviewable interface.

Use a minimal example first, then add one concern at a time. Mixing templates, threads, I/O, allocators, device APIs, packages and deployment in the first experiment hides the cause of errors. A small example is valuable when it exposes the exact rule or transition being studied.

Execution and system model

Trace translation units, declarations, types, values, object construction, function calls, resource acquisition, exceptions, destruction and observable output. Apply this model to Runtime Polymorphism. Draw the relevant values, objects, functions, threads, resources or distributed participants and label every state-changing edge. Mark each construction, move, copy, borrow, synchronization edge and destruction, plus every point where a component assumes ownership of memory, a file, socket, lock, thread, device or message.

C++ expressions have types and value categories; objects occupy storage for a lifetime; references and pointers can alias objects without owning them. Function calls bind arguments after overload resolution and conversions, can return values or throw, and create automatic objects whose destructors run during normal return or stack unwinding. Undefined behavior removes the program from the language's guarantees and must never be treated as an ordinary error result.

Separate language rules from implementation behavior. The language mode selects a standard dialect; compiler flags affect diagnostics, optimization, exceptions, RTTI and code generation; the standard library supplies concrete facilities; the ABI controls binary boundaries; the operating system and hardware define system interfaces and performance. State every relevant layer.

For external data, validate length, range, encoding, shape, authorization and resource cost before effect. Files, serialized objects, packets, database rows, environment variables, command arguments and device input remain untrusted after parsing. Parsing is not validation, and successful conversion does not establish a domain invariant.

Implementation method

Use modern standard-library types, initialize objects, prefer value semantics and RAII, express constness, check boundaries and compile with strong warnings. Start with a one-sentence contract for Runtime Polymorphism. Name input, output, effects, failure, ownership, lifetime and cleanup. For concurrent work include synchronization, cancellation and progress. For binary interfaces include layout, calling convention, allocation and versioning. For remote work include framing, timeouts, retry, idempotency and backpressure.

Choose representations that make invalid states difficult. Use value types, scoped enums, const-correct interfaces, variants, optionals, spans, smart pointers and dedicated exceptions when they clarify the contract. Convert at boundaries and keep internal code working with validated values. Avoid undocumented pointer-length pairs and magic integral states.

Implement a small normal path and make it observable. Return values instead of printing from domain logic. Pass clocks, random sources, storage, transports and device interfaces explicitly instead of reading hidden global state. Diagnostics should identify safe operation context without exposing secrets or personal data.

Add failure behavior deliberately. Reject invalid input early, preserve exception context, roll back state, release resources and avoid publishing partial results. Catch only failures the current layer can recover from or translate meaningfully. A broad catch belongs at a deliberate thread, task or process boundary, not around every function.

Refactor after tests protect behavior. Extract cohesive services, clarify names, remove duplication, narrow public surfaces and document version or runtime assumptions. Static analysis and formatting improve consistency, but they complement behavioral and integration evidence.

Worked C++ example

Read the example before running it. Predict values, value categories, object lifetimes, output, exceptions, external effects and cleanup. Identify dependencies on a C++ language version, library, compiler, ABI, operating system, architecture or untrusted value.

#include <cassert>
#include <memory>
#include <string>
#include <utility>

class Formatter {
public:
virtual ~Formatter() = default;
[[nodiscard]] virtual std::string format(int value) const = 0;
};

class LabelFormatter final : public Formatter {
public:
explicit LabelFormatter(std::string label) : label_(std::move(label)) {}
[[nodiscard]] std::string format(int value) const override {
return label_ + ": " + std::to_string(value);
}
private:
std::string label_;
};

int main()
{
std::unique_ptr<Formatter> formatter = std::make_unique<LabelFormatter>("score");
assert(formatter->format(42) == "score: 42");
}

Trace it from entry to completion. Identify mutable state, aliases, owners and observers. State loop or retry termination. For memory, files, tasks, threads, locks and temporary artifacts, mark construction, acquisition, transfer, release and destruction. Distinguish compile-time constraints and diagnostics from runtime enforcement.

Change a central boundary: empty input, one value, duplicate data, invalid representation, maximum size, timeout, allocation failure, permission denial, repeated message, concurrent update, unsupported language mode or missing library. Write the expected value, exception or error, observable state and cleanup before executing.

Make one deliberately broken copy: leave a value uninitialized, return a dangling view, access outside a range, double-own memory, omit synchronization, ignore an error or benchmark optimized-away work. Use a test, warning, sanitizer, analyzer, debugger, trace, profile or invariant assertion to expose it. Remove the unsafe copy after retaining a regression.

Independent verification

The second artifact is a smaller reference or verification path. Compare its assumptions with the primary implementation.

g++ -std=c++20 -Wall -Wextra -Wpedantic -Wconversion -Wshadow lesson.cpp -o lesson
./lesson

clang++ -std=c++20 -Wall -Wextra -Wpedantic lesson.cpp -o lesson-clang
./lesson-clang

# Expected: both executables finish successfully and all assertions hold.

Independent evidence should fail differently. A simple reference can check an optimized implementation. Static analysis can find flows not covered at runtime. Sanitizers can expose invalid memory and concurrency behavior. Protocol tests can inspect messages. Differential builds can compare compilers, optimization levels, libraries and targets. Assembly or intermediate-representation inspection can expose behavior hidden by a source-level API.

Record negative evidence. Timeouts, skips, unsupported platforms, analyzer unknowns, warnings, flaky outcomes and failed replications are not passes. Preserve them as distinct statuses and investigate whether they narrow the supported contract.

Failure modes and security

1. Using an uninitialized value. During a Runtime Polymorphism review, name the violated contract, minimize the failure, repair it and retain a regression test.
2. Accessing outside a range. During a Runtime Polymorphism review, name the violated contract, minimize the failure, repair it and retain a regression test.
3. Confusing ownership with observation. During a Runtime Polymorphism review, name the violated contract, minimize the failure, repair it and retain a regression test.
4. Forgetting virtual destruction through a base interface. During a Runtime Polymorphism review, name the violated contract, minimize the failure, repair it and retain a regression test.
5. Depending on unspecified evaluation or implementation details. During a Runtime Polymorphism review, name the violated contract, minimize the failure, repair it and retain a regression test.

Classify failures by layer: translation, linking, type or value, object lifetime, invariant, external resource, protocol, concurrency, security, deployment or performance. Begin with the diagnostic, exception or signal and the innermost relevant frame. Inspect actual values, types, configuration and state. Change one factor per diagnostic experiment.

Test cleanup directly. Fail after each acquisition and confirm that destructors run, allocations release, files close, temporary files disappear, locks release, threads join or stop and partial messages are not published. RAII works only when ownership is represented by an object with the correct lifetime.

Treat external values as hostile. Use bounded views and checked conversions, validate sizes before allocation, parameterize database commands, encode output for its destination, constrain file paths and avoid shell interpretation. Validate messages before dispatch and check authorization at the protected effect. Integer overflow, truncation and signedness deserve explicit tests.

Protect secrets outside source control. Use established cryptographic libraries, operating-system random facilities, TLS verification, least privilege and managed key rotation. Do not implement cryptographic protocols from primitives unless the project is specifically qualified to do so.

Architecture and maintainability

Readable C++ exposes ownership, dependencies and lifecycle. Keep domain rules independent of frameworks and operating-system adapters, and keep storage and transport behind explicit boundaries. Prefer composition when inheritance does not represent stable substitutability. Avoid hidden global registries and raw owning pointers.

Libraries and packages need directional dependencies. Minimum language mode, compiler support, build options, public API, ABI policy, transitive dependencies and configuration are part of design. A library should not perform surprising I/O or start threads during static initialization.

Document accepted forms, units, ordering, mutability, ownership, blocking, thread or async safety, idempotency, complexity, precision, security requirements, exceptions and compatibility. Examples help, but an example cannot state every constraint.

For frameworks and platform APIs, distinguish application code from adapters. Versioned files and messages deserve schemas. Database records are not automatically domain models. Composition roots should assemble dependencies near the boundary rather than hide them throughout the code.

Testing and diagnostics

Build in a clean directory with warnings enabled; cover normal, empty, boundary and invalid inputs; verify values, output, exceptions, invariants and destruction. Tests should describe behavior through public boundaries. Assert values, responses, database state, messages, files, metrics and exceptions rather than incidental private call sequences.

Create a boundary table for Runtime Polymorphism: typical, empty, singleton, minimum, maximum, just outside, malformed, duplicate, repeated, adversarial and dependency-failure cases. Add allocation failure, iterator invalidation, concurrency, cancellation, large input, architecture differences and long-running state when relevant. Keep a regression for every defect.

Use fixtures to own setup and cleanup. Keep tests order-independent. Control clocks, randomness and external systems through explicit interfaces. Use real compilers, libraries, serializers, databases, protocol clients and devices in integration tests where their behavior is a risk; mocks cannot prove integration.

Static analysis, coding standards, unit tests, property tests, integration tests, security scanning, mutation testing, profiling and production telemetry answer different questions. State the coverage and blind spots of each tool selected for Runtime Polymorphism.

When debugging, minimize the program and input. Inspect deduced types, value categories, overload candidates, object lifetimes, iterator validity, thread state, generated messages, compiler flags, linked libraries, sanitizer reports and assembly as appropriate. A minimal reproducer becomes a durable test.

Performance and resources

Choose algorithms and data representations before micro-optimizing syntax. Establish a correctness oracle and representative workload. Record compiler and standard-library versions, language mode, optimization and code-generation flags, linker, dependencies, operating system, target architecture, hardware, CPU governor, warm-up, trial order and all observations.

Distinguish latency, throughput, memory, CPU, I/O, database, network, startup and cost. A gain in one can worsen another. Profile to locate dominant work, then test a mechanism. A tiny timing loop teaches mechanics but does not justify a production conclusion without calibration and uncertainty.

Common improvements reduce total work, select a better algorithm, improve data locality, batch I/O, avoid unnecessary allocation and copying, vectorize suitable loops, reduce contention and move cold work from latency-sensitive paths. Keep a clear reference and equivalence tests.

Bound input sizes, allocations, recursion, queues, concurrency, retries, buffers, logs, temporary storage and cache growth. Backpressure and admission control are correctness features when arrivals can exceed capacity.

Practice questions

1. Define Runtime Polymorphism and identify its C++ language, library, ABI, platform or hardware contract.
2. List involved values, objects, services and resources and draw their relationships.
3. Write input, output, effects, exceptions, ownership, cleanup and compatibility rules.
4. Predict the worked example before executing it.
5. Separate portable C++ behavior from a compiler, library, ABI, operating-system or hardware detail.
6. Add empty, typical, boundary, malformed and adversarial tests.
7. Identify a coercion, comparison, reference or mutability risk.
8. Force the earliest dependency failure and prove cleanup.
9. Replace hidden time, randomness, globals, filesystem, network, device or database access with a boundary.
10. Add precise types and explain what runtime validation remains necessary.
11. Create a broken variant and choose the best diagnostic tool.
12. Write a property that holds across generated inputs.
13. Threat-model the most dangerous external value.
14. Design a real integration or protocol test.
15. Define a performance metric and representative workload.
16. Record compiler, library, flags, dependencies, target, platform and raw outcomes.
17. Review compatibility across supported language modes, compilers, libraries and architectures.
18. Refactor for clarity while proving unchanged behavior.

Hands-on project

Build a small library or application centered on Runtime Polymorphism. Write a README with supported C++ language modes, compilers, libraries, build steps, interface, inputs, outputs, effects, errors, security, ownership and examples. Use CMake targets for a multi-file project and keep public headers separate from implementation details.

Implement a clear reference first. Add validated boundaries, typed interfaces, dedicated errors, deterministic cleanup and structured results. Separate domain logic from CLI, network, storage and device adapters. Define transactions, authorization, idempotency, cancellation and shutdown where relevant.

Create at least twelve tests: normal, empty, singleton, lower and upper boundary, malformed, repeated, dependency failure, cleanup, security rejection, compatibility and regression. Add integration tests for real libraries or protocols and run warnings, static analysis and sanitizers appropriate to the level.

Add useful observability without secrets. Provide one command that configures, builds, checks style, analyzes, tests and runs the project. Run it from a clean build directory using pinned dependency revisions or a committed package lock where supported.

Beginners can add a second input form and validation. Intermediate learners can add a generic adapter, coroutine or concurrent component. Advanced learners can add a stable plugin boundary, device or platform adapter, profiling and hardening. Research learners can preregister a hypothesis, compare compilers or algorithms, report uncertainty and publish machine-readable evidence.

Finish with an engineering report describing one defect, the violated invariant, minimized reproducer, repair and regression. Record limitations and a question not answered. The project is complete when another person can reproduce it without private instructions.

Review checklist

Confirm that you can explain Runtime Polymorphism, predict examples, separate language and implementation behavior, validate external data, preserve ownership and cleanup, avoid undefined behavior, test normal and failing behavior, and record compatibility assumptions.

Confirm that resources are bounded, secrets protected, dangerous interpretation avoided, dependencies reproducible and performance measured. Retain practice answers, project, commands, raw evidence and regression cases.

Summary

Runtime Polymorphism belongs to classes, object lifetime, inheritance, polymorphism, templates and exceptions. Runtime Polymorphism examines object invariants, special member functions, lookup, visibility, substitution, dynamic dispatch, lifecycle and extensibility. Dependable work begins with an explicit contract, validated boundaries, typed and cohesive interfaces, deterministic resources, meaningful exceptions and observable behavior.

Examples are a starting point. Boundary tests, sanitizers, failure injection, static analysis, integration work, security review and measurement provide wider evidence. Completion means being able to reproduce results and distinguish ISO C++ behavior from compiler, library, ABI, operating-system and hardware behavior.

Sources and further reading

  • ISO C++ — published standard information and WG21 standardization resources.
  • C++ working draft — normative-style language and standard-library wording.
  • cppreference — cross-version language and standard-library reference.
  • C++ Core Guidelines — modern interface, resource, memory and concurrency guidance.
  • Compiler and standard-library documentation — implementation support, diagnostics and tooling.
  • Applicable operating-system, hardware, protocol and security specifications.

Continue learning

Next recommended topic

Function Overriding