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
EasyLet'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 baseNotifierclass that represents any notification sender:- A protected
std::string recipientmember - 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
- A protected
EmailNotifier.h: Define anEmailNotifierclass that inherits fromNotifier:- A private
std::string domainmember - 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 itfinal— it should return"Email"
Then define an
UrgentEmailNotifierclass that inherits fromEmailNotifier:- 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 markedfinalinEmailNotifier
- A private
main.cpp: Read three inputs (each on a separate line):- Recipient name
- Email domain
- Message text
Create three notifier objects dynamically: a base
Notifier, anEmailNotifier, and anUrgentEmailNotifier— all using the same recipient (and domain where applicable). Store them in an array ofNotifier*pointers.Loop through the array and for each notifier, print its type using
getType(), then callsend()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 3pmNotice 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;
}
This lesson includes a short quiz. Start the lesson to answer it and track your progress.
All lessons in Object Oriented Programming
1Fundamentals of OOP
External FilesC++ Build & CompilationHeader Files & Source FilesNamespaces & ScopeIntroduction to OOP in C++Classes vs ObjectsThe 'this' PointerMethods (Member Functions)Attributes (Data Members)Ctors & Dtors BasicsRecap - Simple Calculator4Class Properties
Instance vs Static MembersGetters and SettersConst Member FunctionsMutable KeywordStatic Methods and VariablesFriend Functions & ClassesRecap - Bank Account Manager7Inheritance
Basic InheritanceInheritance Access LevelsCtor & Dtor Call OrderMethod OverridingVirtual Functions & VTableMultiple InheritanceVirtual InheritanceRecap - Employee Hierarchy2Memory Management
Stack vs Heap MemoryPointers and ReferencesDynamic Memory (new/delete)Smart Pointers in C++RAII in C++Recap - Dynamic Array Manager5Encapsulation
Access Specifiers in C++Access Specifiers In DepthInformation HidingStruct vs ClassNested & Inner ClassesRecap - Student Records System8Polymorphism
Compile vs Runtime PolymorphFunction OverloadingVirtual Functions RevisitedPure Virtual FunctionsAbstract ClassesInterface Design in C++Dynamic Casting & RTTIRecap - Shape Calculator3Constructors & Destructors
Default ConstructorParameterized ConstructorCopy ConstructorMove ConstructorConstructor Init ListsDelegating ConstructorsDestructor Deep DiveRule of Three / Five / ZeroRecap - String Class6Operator Overloading
Intro to Operator OverloadArithmetic Operator OverloadComparison Operator OverloadStream OperatorsAssignment Operator Overload[] and () Operator OverloadType Conversion OperatorsRecap - Matrix Class9Templates
Function TemplatesClass TemplatesTemplate SpecializationVariadic TemplatesSFINAE & Type Traits BasicsRecap - Generic Container