C++ Override and Final Specifiers
Introduction
The override
and final
specifiers were introduced in C++11 to help developers write more robust code when dealing with inheritance and virtual functions. These specifiers provide explicit control over method overriding in derived classes, making code more readable and preventing common bugs related to inheritance.
In this tutorial, you'll learn:
- What the
override
specifier does and how to use it - What the
final
specifier does for both methods and classes - How these specifiers can prevent subtle bugs in your code
- Best practices when working with inheritance hierarchies
The Override Specifier
What Problem Does override
Solve?
In C++, when you inherit from a base class, you can override virtual methods to provide different implementations in derived classes. However, before C++11, it was easy to accidentally create a new method instead of overriding an existing one, as shown in the example below:
class Base {
public:
virtual void show() {
std::cout << "Base class" << std::endl;
}
};
class Derived : public Base {
public:
// Oops! This is a new method, not an override
// The method signature doesn't match exactly (missing const)
virtual void show() const {
std::cout << "Derived class" << std::endl;
}
};
int main() {
Derived d;
Base* b = &d;
b->show(); // Calls Base::show(), not Derived::show()
return 0;
}
Output:
Base class
In the above example, the developer intended to override show()
, but accidentally created a new method because the signatures don't match exactly (one has const
and the other doesn't).
Using the override
Specifier
The override
specifier tells the compiler that a method is intended to override a virtual method from a base class. If it doesn't actually override anything, the compiler will generate an error:
class Base {
public:
virtual void show() {
std::cout << "Base class" << std::endl;
}
};
class Derived : public Base {
public:
// Compiler error: no virtual method in base class matches this signature
void show() const override {
std::cout << "Derived class" << std::endl;
}
};
Corrected version:
class Base {
public:
virtual void show() {
std::cout << "Base class" << std::endl;
}
};
class Derived : public Base {
public:
// Now correctly overrides Base::show()
void show() override {
std::cout << "Derived class" << std::endl;
}
};
int main() {
Derived d;
Base* b = &d;
b->show(); // Now calls Derived::show()
return 0;
}
Output:
Derived class
Key Rules for override
:
- It must be used with a function that overrides a virtual function from a base class
- The function signature and return type must exactly match the one in the base class
- The base class function must be declared as
virtual
override
appears at the end of the function declaration, after anyconst
and reference qualifiers
The Final Specifier
The final
specifier has two uses in C++:
- Preventing a virtual method from being overridden in derived classes
- Preventing a class from being used as a base class
Making Methods Final
When applied to a virtual method, final
prevents any derived class from overriding that method:
class Base {
public:
virtual void method1() {
std::cout << "Base::method1()" << std::endl;
}
virtual void method2() final {
std::cout << "Base::method2()" << std::endl;
}
};
class Derived : public Base {
public:
void method1() override {
std::cout << "Derived::method1()" << std::endl;
}
// Error: cannot override final method
// void method2() override {
// std::cout << "Derived::method2()" << std::endl;
// }
};
int main() {
Derived d;
Base* b = &d;
b->method1(); // Calls Derived::method1()
b->method2(); // Calls Base::method2() (can't be overridden)
return 0;
}
Output:
Derived::method1()
Base::method2()
Making Classes Final
When applied to a class, final
prevents any other class from inheriting from it:
class BaseClass final {
public:
void someMethod() {
std::cout << "BaseClass::someMethod()" << std::endl;
}
};
// Error: cannot derive from final class
// class DerivedClass : public BaseClass {
// public:
// void anotherMethod() {
// std::cout << "DerivedClass::anotherMethod()" << std::endl;
// }
// };
Practical Examples
Example 1: Building a Robust Shape Hierarchy
#include <iostream>
#include <vector>
#include <memory>
class Shape {
public:
virtual ~Shape() = default;
virtual double area() const = 0;
virtual void draw() const {
std::cout << "Drawing a shape" << std::endl;
}
};
class Circle final : public Shape {
private:
double radius;
public:
Circle(double r) : radius(r) {}
double area() const override {
return 3.14159 * radius * radius;
}
void draw() const override {
std::cout << "Drawing a circle with radius " << radius << std::endl;
}
};
class Rectangle : public Shape {
private:
double width;
double height;
public:
Rectangle(double w, double h) : width(w), height(h) {}
double area() const override {
return width * height;
}
void draw() const override final {
std::cout << "Drawing a rectangle " << width << "x" << height << std::endl;
}
};
class Square final : public Rectangle {
public:
Square(double side) : Rectangle(side, side) {}
// Can't override draw() because it's final in Rectangle
// Can still override area() if needed (though not necessary here)
double area() const override {
return Rectangle::area(); // Just calls the base implementation
}
};
int main() {
std::vector<std::unique_ptr<Shape>> shapes;
shapes.push_back(std::make_unique<Circle>(5.0));
shapes.push_back(std::make_unique<Rectangle>(4.0, 3.0));
shapes.push_back(std::make_unique<Square>(2.0));
for (const auto& shape : shapes) {
shape->draw();
std::cout << "Area: " << shape->area() << std::endl;
std::cout << "------------------------" << std::endl;
}
return 0;
}
Output:
Drawing a circle with radius 5
Area: 78.5398
------------------------
Drawing a rectangle 4x3
Area: 12
------------------------
Drawing a rectangle 2x2
Area: 4
------------------------
Example 2: Interface Implementation with Override
#include <iostream>
#include <string>
#include <vector>
#include <memory>
// Logger interface
class Logger {
public:
virtual ~Logger() = default;
virtual void log(const std::string& message) = 0;
virtual void setLevel(int level) = 0;
virtual int getLevel() const = 0;
};
// ConsoleLogger implementation
class ConsoleLogger : public Logger {
private:
int logLevel;
public:
ConsoleLogger() : logLevel(0) {}
void log(const std::string& message) override {
std::cout << "[Console] " << message << std::endl;
}
void setLevel(int level) override {
logLevel = level;
}
int getLevel() const override {
return logLevel;
}
};
// FileLogger implementation
class FileLogger final : public Logger {
private:
int logLevel;
std::string filename;
public:
FileLogger(const std::string& file) : logLevel(0), filename(file) {}
void log(const std::string& message) override {
std::cout << "[File: " << filename << "] " << message << std::endl;
// In a real implementation, we would write to a file here
}
void setLevel(int level) override {
logLevel = level;
}
int getLevel() const override {
return logLevel;
}
};
int main() {
std::vector<std::unique_ptr<Logger>> loggers;
loggers.push_back(std::make_unique<ConsoleLogger>());
loggers.push_back(std::make_unique<FileLogger>("app.log"));
for (const auto& logger : loggers) {
logger->setLevel(1);
logger->log("Application started");
}
return 0;
}
Output:
[Console] Application started
[File: app.log] Application started
Best Practices
-
Always use
override
when overriding virtual functions:- It makes your code more readable
- It prevents subtle bugs
- It documents your intention clearly
-
Use
final
when:- You need to prevent further derivation for security/design reasons
- You're working on performance-critical code (the compiler can optimize non-virtual calls)
- You're building a class that isn't designed to be inherited from
-
Consider the inheritance hierarchy carefully:
- Don't mark methods as
final
unless you have a good reason - Classes marked as
final
can't be mocked in some testing frameworks
- Don't mark methods as
How the Compiler Uses These Specifiers
Summary
- The
override
specifier ensures that a method is actually overriding a base class method - The
final
specifier prevents further overriding of methods or inheritance of classes - Both specifiers help catch errors at compile time rather than at runtime
- Using these specifiers makes code more robust and self-documenting
- These features are part of modern C++ best practices for inheritance hierarchies
Exercises
- Convert an existing class hierarchy in your codebase to use
override
and see if you catch any bugs - Create a simple plugin system where some methods can be overridden but others are marked as
final
- Design a class hierarchy for a game with different character types, using
override
andfinal
appropriately
Additional Resources
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)