You only need to get burned by polymorphism once to respect function overriding.\n\nI still remember reviewing a production bug where a derived class “overrode” a method, tests passed, and yet the base behavior ran in production. The problem wasn’t complicated. It was worse: it was subtle. A missing virtual in the base class and a tiny signature mismatch meant we got function hiding and static binding instead of runtime dispatch. Nothing crashed. The program simply behaved “correctly” according to the compiler—and incorrectly according to the developer.\n\nFunction overriding is one of the core ideas behind object-oriented C++: you define a stable interface in a base class, then redefine behavior in derived classes while keeping the same signature. When you call through a base reference or pointer, the runtime chooses the right implementation for the actual object type.\n\nIf you write modern C++ (C++17 through C++26 drafts), overriding isn’t about showing off inheritance. It’s about expressing contracts, building extensible systems, and avoiding expensive branching logic. Done right, it’s clean. Done carelessly, it’s a quiet bug factory.\n\n## What Function Overriding Really Means (and What It Doesn’t)\nFunction overriding is a derived class redefining a virtual member function from a base class with the same function signature (name, parameter types, cv/ref qualifiers, and—practically—compatible return type). The key property is dynamic dispatch: the choice of which function body runs happens at runtime based on the dynamic type of the object.\n\nOverriding is not the same as:\n\n- Overloading: same name, different parameter list in the same scope.\n- Function hiding: derived class declares a function with the same name (maybe different signature) and hides base overloads.\n- Shadowing: unrelated name hiding rules (often with variables).\n\nThe overriding “contract” that I rely on in real code is:\n\n1. The base function is marked virtual (or overrides a virtual function itself).\n2. The derived function matches the base signature exactly.\n3. Calls through a base pointer/reference dispatch to the derived function.\n\nIf any of those assumptions fail, you’re not doing runtime polymorphism.\n\n### A small, correct baseline\ncpp\n#include \n#include \n#include \n\nclass Animal {\npublic:\n virtual ~Animal() = default; // Base destructors should be virtual for polymorphic deletion.\n\n virtual void sound() const {\n std::cout << "Animal makes a sound\n";\n }\n};\n\nclass Dog : public Animal {\npublic:\n void sound() const override {\n std::cout << "Dog barks\n";\n }\n};\n\nint main() {\n std::vector<std::uniqueptr> animals;\n animals.pushback(std::makeunique());\n animals.pushback(std::makeunique());\n\n for (const auto& a : animals) {\n a->sound();\n }\n}\n\n\nThis is the happy path: Animal::sound() is virtual, Dog::sound() matches it exactly, and override forces the compiler to confirm that we truly override something.\n\n## How Dynamic Dispatch Works: Base vs Derived Calls\nA mental model that stays accurate without being overly academic:\n\n- Calling on a base object uses the base implementation (because the object is a base object).\n- Calling on a derived object uses the derived implementation.\n- Calling through a base pointer/reference can call the derived implementation if and only if the base function is virtual.\n\n### Base object vs derived object\ncpp\n#include \n\nclass Printer {\npublic:\n virtual void print() const {\n std::cout << "Printer: generic output\n";\n }\n};\n\nclass LaserPrinter : public Printer {\npublic:\n void print() const override {\n std::cout << "LaserPrinter: crisp output\n";\n }\n};\n\nint main() {\n Printer p;\n LaserPrinter lp;\n\n p.print(); // Printer::print\n lp.print(); // LaserPrinter::print\n}\n\n\n### Base pointer/reference (the normal real-world case)\ncpp\n#include \n\nclass Base {\npublic:\n virtual void display() const {\n std::cout << "Display from Base\n";\n }\n};\n\nclass Derived : public Base {\npublic:\n void display() const override {\n std::cout << "Display from Derived\n";\n }\n};\n\nint main() {\n Derived d;\n Base p = &d;\n p->display(); // Display from Derived\n}\n\n\nThis is the point of overriding: the code at the call site doesn’t need if (type == Derived) checks.\n\n### What changes when you remove virtual\nIf the base function isn’t virtual, the call is bound at compile time based on the static type of the expression.\n\ncpp\n#include \n\nclass Animal {\npublic:\n void sound() const {\n std::cout << "Animal sound\n";\n }\n};\n\nclass Dog : public Animal {\npublic:\n void sound() const { // Not an override. This hides Animal::sound.\n std::cout << "Dog barks\n";\n }\n};\n\nint main() {\n Dog d;\n Animal a = &d;\n a->sound(); // Animal sound\n d.sound(); // Dog barks\n}\n\n\nThat behavior isn’t “wrong.” It’s just not overriding.\n\n## The override Keyword: My Default Rule in 2026\nI treat override as non-negotiable for virtual overrides in production code.\n\nWhy?\n\n- It makes intent explicit at the function declaration.\n- It turns subtle runtime bugs into compile-time errors.\n- It protects you during refactors (renames, signature changes, const-correctness updates).\n\n### The classic mistake that override catches\ncpp\n#include \n\nclass Animal {\npublic:\n virtual void sound() const {\n std::cout << "Animal makes a sound\n";\n }\n};\n\nclass Dog : public Animal {\npublic:\n void Sound() const override { // Error: does not override any base class method\n std::cout << "Dog barks\n";\n }\n};\n\nint main() {}\n\n\nWithout override, that would compile as a new member function Sound() in Dog. Calls through Animal would still use Animal::sound() and you’d be staring at logs wondering why your “override” never runs.\n\n### A more realistic mismatch: const and reference qualifiers\nThis is the one I see in real systems.\n\ncpp\n#include \n\nclass Sensor {\npublic:\n virtual int read() const {\n return 1;\n }\n};\n\nclass TemperatureSensor : public Sensor {\npublic:\n int read() override { // Error: missing const\n return 42;\n }\n};\n\nint main() {}\n\n\nIf you want to override int read() const, your override must also be const.\n\n### Modern workflow note (2026)\nIf you use clang-tidy, enabling checks like modernize-use-override and treating warnings as errors will enforce override consistently. I also like having IDE hints from clangd/Visual Studio or similar tooling; they’re fast at highlighting “this doesn’t override what you think it overrides.” AI-assisted refactors help too, but override is still the hard safety rail.\n\n## Function Hiding: The Sneaky Cousin of Overriding\nFunction hiding deserves its own section because it’s responsible for a lot of “I hate inheritance” complaints.\n\nIf a derived class declares a function with the same name as a base class function, it can hide all overloads of that name from the base class—even if you didn’t mean to.\n\n### Example: hiding overload sets\ncpp\n#include \n#include \n\nclass Logger {\npublic:\n virtual ~Logger() = default;\n\n void log(int code) const {\n std::cout << "code=" << code << "\n";\n }\n\n virtual void log(const std::string& message) const {\n std::cout << "message=" << message << "\n";\n }\n};\n\nclass FileLogger : public Logger {\npublic:\n void log(const std::string& message) const override {\n std::cout << "file: " << message << "\n";\n }\n\n // If we add ANY other overload named log(...) here, we risk hiding Logger::log(int).\n};\n\nint main() {\n FileLogger fl;\n fl.log(std::string("hello"));\n fl.log(7);\n}\n\n\nThis version compiles and works: overload resolution can still find Logger::log(int) because FileLogger did not introduce a different overload set that triggers hiding issues in this exact way. But in practice, derived classes often grow extra overloads (“let’s add log(double)” or “let’s add log(std::stringview)”), and that’s when you get surprised.\n\n### The fix when you want base overloads visible\nUse a using declaration:\n\ncpp\n#include \n#include \n\nclass Logger {\npublic:\n virtual ~Logger() = default;\n\n void log(int code) const {\n std::cout << "code=" << code << "\n";\n }\n\n virtual void log(const std::string& message) const {\n std::cout << "message=" << message << "\n";\n }\n};\n\nclass FileLogger : public Logger {\npublic:\n using Logger::log; // Bring base overloads into scope.\n\n void log(const std::string& message) const override {\n std::cout << "file: " << message << "\n";\n }\n\n void log(double seconds) const {\n std::cout << "file: took " << seconds << "s\n";\n }\n};\n\nint main() {\n FileLogger fl;\n fl.log(7);\n fl.log(std::string("hello"));\n fl.log(0.25);\n}\n\n\nMy rule: if a base class provides an overload set meant to be used by callers, and the derived class adds any overload with the same name, I add using Base::name; unless I have a specific reason not to.\n\n## Real Systems: Interfaces, Plugins, and the Non-Virtual Interface (NVI) Pattern\nOverriding shines when you separate what the system expects (the interface) from how a component behaves (the derived implementations).\n\n### Example: a tiny “processor” interface\nThis resembles plugin systems, pipeline stages, and handler stacks.\n\ncpp\n#include \n#include \n#include \n#include \n#include \n\nclass TextProcessor {\npublic:\n virtual ~TextProcessor() = default;\n\n virtual std::string name() const = 0;\n virtual std::string process(std::string input) const = 0;\n};\n\nclass TrimProcessor : public TextProcessor {\npublic:\n std::string name() const override { return "trim"; }\n\n std::string process(std::string input) const override {\n auto isspace = [](unsigned char c) {\n return std::isspace(c) != 0;\n };\n\n while (!input.empty() && isspace(staticcast(input.front()))) {\n input.erase(input.begin());\n }\n while (!input.empty() && isspace(staticcast(input.back()))) {\n input.popback();\n }\n return input;\n }\n};\n\nclass SuffixProcessor : public TextProcessor {\npublic:\n explicit SuffixProcessor(std::string suffix) : suffix(std::move(suffix)) {}\n\n std::string name() const override { return "suffix"; }\n\n std::string process(std::string input) const override {\n input += suffix;\n return input;\n }\n\nprivate:\n std::string suffix;\n};\n\nint main() {\n std::vector<std::uniqueptr> pipeline;\n pipeline.pushback(std::makeunique());\n pipeline.pushback(std::makeunique("v2"));\n\n std::string s = " config ";\n for (const auto& p : pipeline) {\n s = p->process(std::move(s));\n std::cout <name() << ": " << s << "\n";\n }\n}\n\n\nThis is practical overriding: your core system knows about TextProcessor, and new behavior ships as derived classes.\n\n### The Non-Virtual Interface pattern\nA lot of C++ codebases benefit from NVI: expose a stable public method that does validation, metrics, locking, logging, timing, etc., then call a private/protected virtual “implementation hook.”\n\ncpp\n#include \n#include \n\nclass PaymentGateway {\npublic:\n virtual ~PaymentGateway() = default;\n\n // Public API: consistent checks and tracing.\n bool charge(int cents, const std::string& currency) {\n if (cents <= 0) return false;\n if (currency.empty()) return false;\n\n // Cross-cutting concerns belong here (logging, metrics, rate limits).\n return chargeimpl(cents, currency);\n }\n\nprivate:\n virtual bool chargeimpl(int cents, const std::string& currency) = 0;\n};\n\nclass SandboxGateway : public PaymentGateway {\nprivate:\n bool chargeimpl(int cents, const std::string& currency) override {\n std::cout << "[sandbox] charged " << cents << " " << currency << "\n";\n return true;\n }\n};\n\nint main() {\n SandboxGateway g;\n g.charge(199, "USD");\n}\n\n\nI like NVI because it reduces the chance that derived classes “forget” critical preconditions. The public method is the contract. The override is just the implementation hook.\n\n### A simple analogy for overriding\nIf you want a non-code analogy, think of a legal framework: a base set of principles defines the structure, and a later system can adopt those principles but restate them in a new context with local details. The shape stays recognizable; the implementation changes. Overriding works the same way: the interface stays stable, the behavior becomes specific.\n\n## Performance and Memory: What Virtual Dispatch Costs (and When It Matters)\nVirtual calls have overhead, but I rarely see it as the bottleneck unless:\n\n- You’re calling virtual methods in an inner loop measured in billions of iterations.\n- You’re doing heavy work with tiny functions that otherwise would inline.\n- You’re targeting extremely constrained environments.\n\nHere’s the practical cost model:\n\n- Time: a virtual call usually involves an extra indirection (load vptr, fetch function pointer, indirect call). In many real workloads, this is drowned out by cache misses, allocations, I/O, and actual business logic.\n- Memory: polymorphic objects typically carry a vptr (often one machine word). Each polymorphic class has a vtable (shared, not per-object).\n- Inlining: the compiler often can’t inline through a virtual call (unless it can devirtualize based on whole-program knowledge or final types).\n\nIn performance-sensitive code, I reach for:\n\n- final on classes or functions when extension isn’t needed.\n- composition (strategy objects passed explicitly) for hot paths.\n- templates and std::variant for static polymorphism when the set of types is known.\n\n### Traditional vs modern choice points\n
Traditional OOP approach
\n
—
\n
virtual interface + overrides
override, final, NVI, and strict ownership (uniqueptr) \n
virtual dispatch
std::variant + std::visit or templates \n
override public method
\n
documentation
final on class or specific virtual function \n\nThis isn’t ideology. I pick runtime polymorphism when I need runtime flexibility, and I pick static alternatives when the set of behaviors is fixed and performance is the main goal.\n\n## final: Preventing Overrides (and Helping the Compiler)\nfinal does two related things:\n\n1. Marks a virtual function so it cannot be overridden further.\n2. Marks a class so it cannot be inherited from.\n\n### final on a virtual function\ncpp\n#include \n\nclass Base {\npublic:\n virtual void show() const final {\n std::cout << "Base show\n";\n }\n};\n\nclass Derived : public Base {\npublic:\n void show() const override { // Error: cannot override final function\n std::cout << "Derived show\n";\n }\n};\n\nint main() {}\n\n\n### final on a class\ncpp\n#include \n\nclass FinalLogger final {\npublic:\n void write(const char msg) const {\n std::cout << msg << "\n";\n }\n};\n\nclass SubLogger : public FinalLogger { // Error: cannot inherit from final\n};\n\nint main() {}\n\n\nWhen do I use final?\n\n- When I’m shipping an API and want to lock down extension points for safety.\n- When I want to document “this is not meant to be subclassed,” and I want that documented in the type system.\n- When I’m optimizing and I want to make devirtualization more likely.\n\nThat last point matters more than people expect: even if you don’t care about micro-optimizations, using final clarifies intent, and intent is half of maintainability.\n\n## The Signature Rules Are Stricter Than Most People Remember\nOverriding in C++ is picky, and that’s a feature. The compiler is protecting you from “almost the same” functions behaving as if they’re the same.\n\nHere’s what effectively participates in the signature for overriding:\n\n- Parameter types (including const, pointers/references, and qualifiers inside the types)\n- const-qualification of the member function (foo() const vs foo())\n- Reference qualifiers on the member function (foo() & vs foo() &&)\n- noexcept (in practice, it must match appropriately; a noexcept mismatch can prevent overriding)\n- Return type compatibility (including covariant returns)\n\n### The “looks the same” bug: const on the member function\nI already showed this once, but it’s so common it’s worth repeating: read() const and read() are different functions. If your base interface is logically const (it should be callable on a const Base&), your override must preserve that.\n\n### Reference qualifiers: & vs && can matter\nRef-qualified member functions are a modern C++ tool for expressing “this operation requires an lvalue object” or “this is only meaningful on temporaries.” They also affect overriding.\n\ncpp\n#include \n#include \n\nclass Buffer {\npublic:\n virtual ~Buffer() = default;\n\n virtual std::string view() const & {\n return "Buffer view (lvalue)";\n }\n\n virtual std::string view() const && {\n return "Buffer view (rvalue)";\n }\n};\n\nclass FastBuffer : public Buffer {\npublic:\n std::string view() const & override {\n return "FastBuffer view (lvalue)";\n }\n\n std::string view() const && override {\n return "FastBuffer view (rvalue)";\n }\n};\n\nint main() {\n FastBuffer b;\n Buffer& br = b;\n\n std::cout << br.view() << "\n";\n std::cout << std::move(br).view() << "\n";\n}\n\n\nIf you only override one of these overloads, you can accidentally “half override” behavior depending on whether the object is an lvalue or rvalue at the call site. That can be totally correct—or totally surprising—depending on your design.\n\n### noexcept is part of the contract\nIn modern C++ APIs, noexcept is documentation with teeth. If a base class promises something doesn’t throw, a derived implementation shouldn’t quietly weaken that. Treat noexcept as an interface constraint, not a decoration.\n\n## Covariant Return Types: The One Exception People Like\nC++ allows a specific kind of return-type change for overrides called a covariant return. This only works when the return types are pointers or references to classes, and the derived return type is a pointer/reference to a class derived from the base return type.\n\nWhy it’s useful: it lets derived classes return a more specific type without forcing callers to cast.\n\ncpp\n#include \n#include \n\nclass Animal {\npublic:\n virtual ~Animal() = default;\n virtual Animal clone() const {\n return new Animal(this);\n }\n};\n\nclass Dog : public Animal {\npublic:\n Dog clone() const override {\n return new Dog(this);\n }\n};\n\nint main() {\n Dog d;\n std::uniqueptr a(d.clone());\n std::uniqueptr dd(d.clone());\n\n std::cout << "cloned\n";\n}\n\n\nA few practical notes:\n\n- Covariant returns are a convenience, not a substitute for good ownership design. If you expose raw pointers, decide who owns them. In new code, I usually prefer returning std::uniqueptr from clone() so ownership is explicit and consistent across the hierarchy.\n- Covariance does not apply to unrelated return types (e.g., int vs long, or std::string vs std::stringview). Those must match exactly (modulo normal override rules).\n\n## Default Arguments + Virtual Functions: A Trap You Should Recognize\nThis one bites even experienced developers because everything “looks” virtual, but only half of it is.\n\nDefault arguments are bound at compile time based on the static type of the call expression. Virtual dispatch chooses the function body at runtime based on the dynamic type. Mix them and you can get “derived behavior with base defaults.”\n\ncpp\n#include \n\nclass Base {\npublic:\n virtual ~Base() = default;\n\n virtual void print(int level = 1) const {\n std::cout << "Base level=" << level << "\n";\n }\n};\n\nclass Derived : public Base {\npublic:\n void print(int level = 2) const override {\n std::cout << "Derived level=" << level << "\n";\n }\n};\n\nint main() {\n Derived d;\n Base b = &d;\n\n d.print(); // Derived level=2\n b->print(); // Derived level=1 (body is Derived, default arg is Base)\n}\n\n\nHow I avoid this:\n\n- Don’t use default arguments on virtual functions.\n- If you need convenience overloads, put them in the non-virtual public interface (NVI-style) and funnel into a virtual function with explicit parameters.\n\n## Object Slicing: Overriding Can’t Save You If You Copy by Value\nIf you store a derived object in a base object by value, you slice off the derived part. Then there is no derived object left for dynamic dispatch to “discover.”\n\ncpp\n#include \n#include \n\nclass Shape {\npublic:\n virtual ~Shape() = default;\n virtual void draw() const {\n std::cout << "Shape\n";\n }\n};\n\nclass Circle : public Shape {\npublic:\n void draw() const override {\n std::cout << "Circle\n";\n }\n};\n\nint main() {\n std::vector shapes;\n shapes.pushback(Circle{}); // Slices Circle into Shape\n\n shapes[0].draw(); // Shape\n}\n\n\nThis is one of those bugs that feels like the language betrayed you until you internalize the rule: polymorphism in C++ is mostly about pointers/references. If you want a heterogeneous container, you typically store std::uniqueptr (or std::sharedptr when shared ownership is truly needed).\n\n## Polymorphic Destruction: Why virtual ~Base() Isn’t Optional\nIf you intend to delete derived objects through a base pointer, the base destructor must be virtual. Otherwise you get undefined behavior (usually: derived destructor doesn’t run, resources leak, invariants break).\n\ncpp\n#include \n\nclass Base {\npublic:\n // Try removing virtual and watch what happens conceptually.\n virtual ~Base() {\n std::cout << "~Base\n";\n }\n};\n\nclass Derived : public Base {\npublic:\n ~Derived() override {\n std::cout << "~Derived\n";\n }\n};\n\nint main() {\n Base b = new Derived();\n delete b; // Must call ~Derived then ~Base\n}\n\n\nMy mental shortcut: if a class has any virtual function, it should almost certainly have a virtual destructor too (even if defaulted).\n\n## Access Control and Overriding: You Can Override Private Virtuals\nThis surprises people: a derived class can override a virtual function even if it’s declared private in the base class. Access control is checked at the call site, not at the override site.\n\nWhy you’d do this: it’s a technique to enforce an NVI-like structure, where the public API calls into a private virtual “hook” that derived classes implement, but callers can’t invoke directly.\n\ncpp\n#include \n\nclass Base {\npublic:\n virtual ~Base() = default;\n\n void run() {\n std::cout << "Base::run setup\n";\n step();\n std::cout << "Base::run teardown\n";\n }\n\nprivate:\n virtual void step() {\n std::cout << "Base::step\n";\n }\n};\n\nclass Derived : public Base {\nprivate:\n void step() override {\n std::cout << "Derived::step\n";\n }\n};\n\nint main() {\n Derived d;\n d.run();\n}\n\n\nCallers cannot call step() (it’s private in Base), but Base::run() can, and it will dynamically dispatch to Derived::step().\n\n## Multiple Inheritance: override Becomes Even More Valuable\nMultiple inheritance is not automatically evil, but it increases the odds of ambiguous overrides, diamond shapes, and “which base are we talking about?” confusion. In that environment, override isn’t just a nice-to-have—it’s a sanity check.\n\nA practical multiple inheritance case is composing independent interfaces (“serializable” + “loggable”).\n\ncpp\n#include \n#include \n\nstruct Serializable {\n virtual ~Serializable() = default;\n virtual std::string serialize() const = 0;\n};\n\nstruct Loggable {\n virtual ~Loggable() = default;\n virtual void log(std::ostream& os) const = 0;\n};\n\nclass User : public Serializable, public Loggable {\npublic:\n explicit User(std::string name) : name(std::move(name)) {}\n\n std::string serialize() const override {\n return "{\\"name\\":\\"" + name + "\\"}";\n }\n\n void log(std::ostream& os) const override {\n os << "User(" << name << ")";\n }\n\nprivate:\n std::string name;\n};\n\nint main() {\n User u("Ava");\n Serializable s = &u;\n Loggable l = &u;\n\n std::cout <serialize() <log(std::cout);\n std::cout << "\n";\n}\n\n\nWhen multiple inheritance is used like this—small pure interfaces, no shared state—it tends to be manageable. Where it becomes risky is when you have stateful base classes with overlapping responsibilities. In those designs, I usually step back and reconsider composition.\n\n## Pure Virtual Functions and Abstract Bases: Overriding as a Requirement\nMany codebases treat “interface” as “abstract base class with pure virtual methods.” In C++ terms, that’s a base class with at least one pure virtual (= 0) function.\n\ncpp\n#include \n#include \n#include \n\nclass Storage {\npublic:\n virtual ~Storage() = default;\n virtual void put(std::string key, std::string value) = 0;\n virtual std::string get(const std::string& key) const = 0;\n};\n\nclass InMemoryStorage : public Storage {\npublic:\n void put(std::string key, std::string value) override {\n data[std::move(key)] = std::move(value);\n }\n\n std::string get(const std::string& key) const override {\n auto it = data.find(key);\n return it == data.end() ? "" : it->second;\n }\n\nprivate:\n std::unorderedmap data;\n};\n\nint main() {}\n\n\nTwo practical notes I wish more people said out loud:\n\n- Abstract bases still need good design. If your pure virtual interface forces derived classes to duplicate validation, or exposes methods that must be called in a specific order, you’re going to pay for it in bugs. NVI can help.\n- Don’t confuse “interface” with “no data.” Sometimes a base class with shared state is correct. But then you’re not modeling a pure interface—you’re modeling a common implementation, and that changes how you should test and reason about overrides.\n\n## When Overriding Is the Wrong Tool\nOverriding is powerful, but I don’t treat it as the default solution to every “different behavior” problem. I ask a few questions first.\n\n### 1) Do I need runtime extensibility?\nIf the set of behaviors is fixed and known at compile time, static polymorphism might be better. Two popular modern options:\n\n- Templates (compile-time strategy selection)\n- std::variant + std::visit (sum types, closed set of alternatives)\n\n### 2) Is this really substitutability?\nOverriding is about substituting a derived object wherever a base is expected. If the derived class can’t honor the base contract (preconditions, postconditions, invariants), you’re building a trap.\n\n### 3) Is the base class stable?\nA base class is a promise. Every new virtual method or signature change can ripple across derived classes. If you don’t control derived classes (plugins, external users, separate teams), design the base as if it’s an API you’ll live with for years.\n\n### 4) Would composition reduce the surface area?\nSometimes it’s cleaner to make a class hold a strategy object and forward calls, rather than inheriting. You can still use overriding—but at the strategy boundary, not across your whole domain model.\n\n## Debugging Overriding Bugs: What I Actually Check First\nWhen someone says “my override isn’t being called,” I run a simple checklist before I do anything fancy.\n\n1. Is the base function actually virtual? (Or is it a non-virtual function with the same name?)\n2. Does the derived declaration have override? If not, add it and see what the compiler says.\n3. Is the call happening through Base* / Base&? If you’re calling on a sliced base value, you’ll never dispatch to derived.\n4. Does the signature match exactly? Look for: missing const, different parameter types, ref qualifiers, noexcept differences.\n5. Is there function hiding due to overload sets? Do you need using Base::name;?\n6. Are you accidentally calling the base version explicitly (e.g., Base::sound() inside a derived method)?\n\nIf you do just one thing consistently, make it this: write override on every override and final when appropriate. It turns most debugging sessions into compile errors.\n\n## Practical Patterns That Make Overrides Safer\nHere are patterns I’ve seen improve codebases that heavily use overriding.\n\n### Pattern 1: Keep public virtual interfaces small\nThe bigger your virtual interface, the more places derived classes can violate assumptions. Prefer a few powerful operations over a dozen tiny ones, unless the domain really demands it.\n\n### Pattern 2: Prefer “template method” style (NVI) for correctness\nIf you have logging, validation, locking, caching, metrics, tracing, retries, circuit breakers—put that in non-virtual wrappers. Make the override a narrow “do the core work” hook.\n\n### Pattern 3: Make base contracts explicit\nI’m not asking for a 10-page spec. I mean things like:\n\n- “process() must be thread-safe.”\n- “read() must not block.”\n- “serialize() must produce valid UTF-8.”\n\nWhen those constraints are real, write them down in comments and tests. Overriding is where assumptions go to die if they aren’t made explicit.\n\n### Pattern 4: Choose ownership consciously\nPolymorphic hierarchies almost always imply heap allocation at the boundary. Use std::uniqueptr when possible; use std::sharedptr only when you truly have shared ownership. Don’t pass raw owning pointers around and call it “fine because it’s C++.”\n\n## A Bigger Real-World Example: A Pluggable Exporter with NVI and Testing Hooks\nThis is the kind of design I end up with in production: a stable public API, a narrow overridable surface, and consistent error handling.\n\ncpp\n#include \n#include \n#include \n#include \n#include \n\nstruct Record {\n int id;\n std::string name;\n};\n\nclass Exporter {\npublic:\n virtual ~Exporter() = default;\n\n // Non-virtual interface: consistent validation + formatting invariants.\n bool exportall(const std::vector& records, std::ostream& out) {\n if (!out.good()) return false;\n if (records.empty()) {\n // Decide what you want here: empty output could be fine.\n // I still route through impl for consistent headers/footers.\n }\n\n begin(out);\n for (const auto& r : records) {\n if (!writerecord(r, out)) {\n end(out);\n return false;\n }\n }\n end(out);\n return out.good();\n }\n\nprotected:\n // Protected virtual hooks: derived classes override these.\n virtual void begin(std::ostream& out) {\n (void)out;\n }\n\n virtual bool writerecord(const Record& r, std::ostream& out) = 0;\n\n virtual void end(std::ostream& out) {\n (void)out;\n }\n};\n\nclass CsvExporter : public Exporter {\nprotected:\n void begin(std::ostream& out) override {\n out << "id,name\n";\n }\n\n bool writerecord(const Record& r, std::ostream& out) override {\n // Very naive CSV escaping (example only).\n out << r.id << "," << r.name << "\n";\n return out.good();\n }\n};\n\nclass JsonLinesExporter : public Exporter {\nprotected:\n bool writerecord(const Record& r, std::ostream& out) override {\n out << "{\\"id\\":" << r.id << ",\\"name\\":\\"" << r.name << "\\"}\n";\n return out.good();\n }\n};\n\nint main() {\n std::vector records = {\n {1, "Ada"},\n {2, "Linus"},\n };\n\n std::uniqueptr e1 = std::makeunique();\n std::uniqueptr e2 = std::makeunique();\n\n std::ostringstream csv;\n std::ostringstream jsonl;\n\n e1->exportall(records, csv);\n e2->exportall(records, jsonl);\n\n std::cout << "CSV:\n" << csv.str() << "\n";\n std::cout << "JSONL:\n" << jsonl.str() << "\n";\n}\n\n\nWhy I like this design:\n\n- The public method exportall() stays stable and enforces sequencing (begin then many records then end).\n- Derived classes override narrow hooks (writerecord, optionally headers/footers).\n- You can test the base workflow once (e.g., “end() is called even on error”) and then test derived formatting separately.\n\nIf you’ve ever dealt with derived classes forgetting to call Base::init() or forgetting to emit a footer, NVI feels like relief.\n\n## Common Pitfalls (A Short List I Wish Every Team Had on a Wiki)\n- Forgetting virtual in the base class, then wondering why derived behavior never runs through base pointers.\n- Forgetting override in the derived class, turning “typos” into new methods instead of compile errors.\n- Signature mismatch due to const, ref qualifiers, noexcept, or parameter type drift.\n- Function hiding of overload sets; fixed with using Base::name; when appropriate.\n- Using default arguments on virtual functions and getting “derived body + base defaults.”\n- Object slicing by storing derived objects by value in a base container.\n- Missing virtual destructor for polymorphic deletion.\n\n## A Practical Rule Set I Actually Follow\nIf I had to reduce all of this into habits you can enforce in code review, it would be these:\n\n1. Every override uses override.\n2. Base classes meant for polymorphism have virtual ~Base() = default;.\n3. Avoid default arguments on virtual functions.\n4. Prefer NVI when there are invariants, sequencing, or cross-cutting concerns.\n5. Use final when you don’t want extension.\n6. Avoid value semantics in polymorphic hierarchies (no slicing). Use smart pointers intentionally.\n7. If you see function name reuse with overloads, consider using Base::name; to prevent hiding surprises.\n\n## Expansion Strategy\nAdd new sections or deepen existing ones with:\n- Deeper code examples: More complete, real-world implementations\n- Edge cases: What breaks and how to handle it\n- Practical scenarios: When to use vs when NOT to use\n- Performance considerations: Before/after comparisons (use ranges, not exact numbers)\n- Common pitfalls: Mistakes developers make and how to avoid them\n- Alternative approaches: Different ways to solve the same problem\n\n## If Relevant to Topic\n- Modern tooling and AI-assisted workflows (for infrastructure/framework topics)\n- Comparison tables for Traditional vs Modern approaches\n- Production considerations: deployment, monitoring, scaling\n\nKeep existing structure. Add new H2 sections naturally. Use first-person voice.


