Menu
Coddy logo textTech

Virtual Functions Revisited

Part of the Object Oriented Programming section of Coddy's C++ journey — lesson 58 of 104.

Now that we understand the difference between compile-time and runtime polymorphism, let's take a deeper look at virtual functions and the override keyword that makes them safer to use.

When you mark a function as virtual in a base class, derived classes can provide their own implementation. The override specifier explicitly tells the compiler that you intend to override a virtual function:

class Animal {
public:
    virtual void speak() {
        std::cout << "Some sound" << std::endl;
    }
    virtual ~Animal() = default;
};

class Dog : public Animal {
public:
    void speak() override {
        std::cout << "Woof!" << std::endl;
    }
};

Using override is crucial because it catches errors at compile time. If you accidentally misspell the function name or use wrong parameters, the compiler will alert you instead of silently creating a new function:

class Cat : public Animal {
public:
    void speek() override {  // Compiler error: no function to override
        std::cout << "Meow!" << std::endl;
    }
};

The final specifier prevents further overriding. Use it when a derived class should be the last one to override a particular function:

class Bulldog : public Dog {
public:
    void speak() override final {
        std::cout << "Gruff woof!" << std::endl;
    }
};

class TinyBulldog : public Bulldog {
    void speak() override {}  // Error: cannot override final function
};

Always use override when overriding virtual functions. It documents your intent and lets the compiler verify that you're actually overriding an existing virtual function.

challenge icon

Challenge

Easy

Let's build a notification system that demonstrates the power of override and final keywords with virtual functions. You'll create a hierarchy of notification handlers where some methods can be further customized and others are locked down to prevent modification.

You'll organize your code across three files:

  • Notifier.h: Define a base Notifier class that represents any notification sender:
    • A protected std::string recipient member
    • A constructor that takes and stores the recipient name
    • A virtual send(const std::string& message) method that prints: Notifying <recipient>: <message>
    • A virtual getType() method that returns the string "Generic"
    • A virtual destructor
  • EmailNotifier.h: Define an EmailNotifier class that inherits from Notifier:
    • A private std::string domain member
    • A constructor that takes recipient and domain, passing recipient to the base class
    • Override send() to print: Emailing <recipient>@<domain>: <message>
    • Override getType() and mark it final — it should return "Email"

    Then define an UrgentEmailNotifier class that inherits from EmailNotifier:

    • A constructor that takes recipient and domain, passing both to EmailNotifier
    • Override send() to print: [URGENT] Emailing <recipient>@<domain>: <message>
    • Note: You cannot override getType() here because it was marked final in EmailNotifier
  • main.cpp: Read three inputs (each on a separate line):
    1. Recipient name
    2. Email domain
    3. Message text

    Create three notifier objects dynamically: a base Notifier, an EmailNotifier, and an UrgentEmailNotifier — all using the same recipient (and domain where applicable). Store them in an array of Notifier* pointers.

    Loop through the array and for each notifier, print its type using getType(), then call send() with your message. Format each entry as:

    Type: <type>
    <send output>

    Print a blank line between each notifier. Clean up your dynamically allocated objects when done.

For example, with inputs Alice, company.com, and Meeting at 3pm:

Type: Generic
Notifying Alice: Meeting at 3pm

Type: Email
Emailing Alice@company.com: Meeting at 3pm

Type: Email
[URGENT] Emailing Alice@company.com: Meeting at 3pm

Notice how UrgentEmailNotifier can override send() to customize the message format, but it inherits the "Email" type from EmailNotifier because getType() was marked final. Use the override keyword on all overridden methods to catch any signature mismatches at compile time.

Cheat sheet

The override specifier explicitly tells the compiler that you intend to override a virtual function from a base class:

class Animal {
public:
    virtual void speak() {
        std::cout << "Some sound" << std::endl;
    }
    virtual ~Animal() = default;
};

class Dog : public Animal {
public:
    void speak() override {
        std::cout << "Woof!" << std::endl;
    }
};

Using override catches errors at compile time. If you misspell the function name or use wrong parameters, the compiler will alert you:

class Cat : public Animal {
public:
    void speek() override {  // Compiler error: no function to override
        std::cout << "Meow!" << std::endl;
    }
};

The final specifier prevents further overriding in derived classes:

class Bulldog : public Dog {
public:
    void speak() override final {
        std::cout << "Gruff woof!" << std::endl;
    }
};

class TinyBulldog : public Bulldog {
    void speak() override {}  // Error: cannot override final function
};

Always use override when overriding virtual functions to document your intent and enable compiler verification.

Try it yourself

#include <iostream>
#include <string>
#include "Notifier.h"
#include "EmailNotifier.h"

using namespace std;

int main() {
    // Read inputs
    string recipient;
    string domain;
    string message;
    
    getline(cin, recipient);
    getline(cin, domain);
    getline(cin, message);
    
    // TODO: Create an array of Notifier* pointers with 3 elements
    
    // TODO: Dynamically create:
    // - A base Notifier with the recipient
    // - An EmailNotifier with recipient and domain
    // - An UrgentEmailNotifier with recipient and domain
    
    // TODO: Loop through the array and for each notifier:
    // - Print "Type: " followed by getType() result
    // - Call send() with the message
    // - Print a blank line between notifiers (not after the last one)
    
    // TODO: Clean up dynamically allocated objects
    
    return 0;
}
quiz iconTest yourself

This lesson includes a short quiz. Start the lesson to answer it and track your progress.

All lessons in Object Oriented Programming