C++ override & final: Virtual Overrides and Devirtualization
Key takeaways
C++ override and final: catching signature mistakes, sealing classes and virtual functions, devirtualization and performance notes, and practical patterns with interfaces.
What is override?
override (C++11) marks a member function as intended to override a base class virtual function. The compiler verifies a matching virtual in a base; typos and signature drift become errors instead of silent new overloads.
class Base {
public:
virtual void func() {
std::cout << "Base::func" << std::endl;
}
};
class Derived : public Base {
public:
void func() override {
std::cout << "Derived::func" << std::endl;
}
};
Why it matters:
- Typos: wrong name → error with
override - Signature checks: parameters,
const, return (covariant rules) - Intent: readers see polymorphic intent immediately
- Refactors: base changes surface errors in derived classes
class Base {
public:
virtual void process(int x) {}
};
class Derived : public Base {
public:
// Without override: typo silently creates a new function
void proccess(int x) {} // bug
// With override: error—nothing to override
void proccess(int x) override {}
};
Mechanics: override tells the compiler “this overrides a base virtual.” The compiler checks:
- A virtual with the same name exists in a base
- Signature matches (parameters, cv/ref qualifiers, return type rules)
- The base function is not
final
class Base {
public:
virtual void func(int x) const {}
};
class Derived : public Base {
public:
// void func(int x) override {} // error: missing const
// void func(double x) const override {} // error: wrong parameter
void func(int x) const override {} // OK
};
The diagnostics are clear enough to act on without guessing. GCC reports 'void Derived::proccess(int)' marked 'override', but does not override, Clang reports 'proccess' marked 'override' but does not override any member functions, and MSVC emits error C3668. When you see one of these after a refactor, the fix is almost never to delete override; it is to find which part of the signature drifted.
Note that the missing-const version is worse than it looks when written without override. It compiles, and Derived now has two functions named func: the inherited const one, which still runs for calls through const Base&, and a new non-const one. It also hides every base overload named func for calls made through Derived, so code that used to call a base overload may now resolve to a different function or fail to compile far away from the real cause.
Virtual override in a nutshell
A virtual member in the base participates in dynamic dispatch. A derived class provides the same signature to override the vtable slot. Calls through base pointers/references invoke the derived implementation.
- vtable: indirect calls through function pointer tables on typical implementations
- Signature match: name, parameters, cv-qualifiers, ref qualifiers, and covariant returns must line up—otherwise you may accidentally add a new function
struct Base {
virtual void f(int) const {}
virtual ~Base() = default;
};
struct Derived : Base {
void f(int) const override {}
};
Why override is important
Without override, these slip through quietly:
- Name typos:
drawvsdrwa - Missing
const: basevoid f() constvs derivedvoid f() - Parameter type changes:
intvssize_t - Base API changes:
virtualremoved or signature changed—derived code may silently stop overriding Withoverride, those cases are compile-time errors, protecting polymorphic contracts.
Case 4 is the one that bites in larger codebases. Someone adds a parameter to a base-class hook, updates the three derived classes they know about, and a fourth class in another module keeps its old signature. Without override everything builds, and that class’s customization simply stops running; the base default executes instead. I have found this kind of bug only by noticing that a log line no longer appeared, which is a slow way to find a signature mismatch the compiler could have reported instantly. Once a class uses override on some members, Clang’s -Winconsistent-missing-override warns about the members that lack it, and GCC’s -Wsuggest-override warns about every overriding function without it. clang-tidy’s modernize-use-override can add the keyword across an existing codebase automatically.
override is a contextual keyword, not a reserved word. It only has special meaning after a member function declarator, so existing code that uses override as a variable or function name still compiles. That is why it was possible to add it in C++11 without breaking old code, and why it has to be written after the parameter list and qualifiers (void f() const override), not in front.
Benefits of override (example)
class Base {
public:
virtual void func(int x) {}
};
class Derived : public Base {
public:
void fucn(int x) {} // typo: new function
void fucn(int x) override {} // error: nothing to override
};
What is final?
final (C++11) forbids further overriding (on a virtual function) or forbids inheriting the class (when applied to the class).
class FinalClass final {
public:
void func() {}
};
// class Derived : public FinalClass {}; // error
class Base {
public:
virtual void func() final {}
};
class Derived : public Base {
public:
// void func() override {} // error: final in base
};
Why use it:
- Design intent: this type or function is not meant to grow further
- Security: sensitive logic cannot be overridden
- Optimization hints: may enable devirtualization
- Safety: prevent unexpected subclasses
class SecurityManager final {
public:
void authenticate() { /* ... */ }
};
Performance: if no further overrides exist, the compiler may devirtualize or inline in some contexts (implementation-dependent; profile hot paths).
final class vs final function:
final class | final function | |
|---|---|---|
| Applies to | Whole class | One virtual function |
| Effect | No inheritance | No further override |
| Position | After class name | After virtual function |
Choosing between them
| Meaning | When | |
|---|---|---|
class D final | Cannot inherit D | You are sure the hierarchy ends here (security, invariants, perf). |
virtual void f() final | f cannot be overridden again | Lock one step of a pipeline while leaving other hooks open. |
final on a class can help optimization by telling the compiler there are no more derived types. final on a function locks one method. Do not overuse if you may need extension later.
The error messages are equally direct: GCC says cannot derive from 'final' base 'FinalClass' in derived type 'Derived' for a sealed class, and virtual function '...' overriding final function for a sealed member. Two limits are worth knowing. final applies only to virtual functions; writing it on a non-virtual member is ill-formed. And final does not stop name hiding: a derived class can still declare a new, unrelated function with the same name and a different signature, which is legal and does not override anything.
The most common practical cost of final shows up in tests. Mocking frameworks such as Google Mock work by deriving from the class under test and overriding its virtual functions, so a final class cannot be mocked directly. If a type is final, tests have to depend on an interface it implements instead, which is usually the better design anyway, but it is a decision to make deliberately rather than by habit.
override in shape, logger, template-method, and database hierarchies
A shape hierarchy
class Shape {
public:
virtual double area() const = 0;
virtual void draw() const = 0;
virtual ~Shape() = default;
};
class Circle : public Shape {
double radius;
public:
Circle(double r) : radius(r) {}
double area() const override {
return 3.14159 * radius * radius;
}
void draw() const override {
std::cout << "Drawing Circle" << std::endl;
}
};
class Rectangle final : public Shape {
double width, height;
public:
Rectangle(double w, double h) : width(w), height(h) {}
double area() const override final {
return width * height;
}
void draw() const override {
std::cout << "Drawing Rectangle" << std::endl;
}
};
// class Square : public Rectangle {}; // error: Rectangle is final
Logger implementations
class Logger {
public:
virtual void log(const std::string& message) {
std::cout << "[LOG] " << message << std::endl;
}
virtual ~Logger() = default;
};
class FileLogger : public Logger {
std::ofstream file;
public:
FileLogger(const std::string& filename) : file(filename) {}
void log(const std::string& message) override {
file << "[FILE] " << message << std::endl;
}
};
class SecureLogger final : public FileLogger {
public:
SecureLogger(const std::string& filename) : FileLogger(filename) {}
void log(const std::string& message) override final {
FileLogger::log("ENCRYPTED: " + message);
}
};
Template method with overridable steps
class GameCharacter {
public:
void takeDamage(int damage) {
beforeDamage();
applyDamage(damage);
afterDamage();
}
virtual ~GameCharacter() = default;
protected:
virtual void beforeDamage() {}
virtual void applyDamage(int damage) = 0;
virtual void afterDamage() {}
};
class Warrior : public GameCharacter {
int health = 100;
int armor = 50;
protected:
void beforeDamage() override {
std::cout << "brace" << std::endl;
}
void applyDamage(int damage) override final {
int actual = std::max(0, damage - armor);
health -= actual;
std::cout << "damage: " << actual << std::endl;
}
void afterDamage() override {
if (health <= 0) {
std::cout << "warrior down" << std::endl;
}
}
};
Database interfaces
class IDatabase {
public:
virtual void connect() = 0;
virtual void disconnect() = 0;
virtual void query(const std::string& sql) = 0;
virtual ~IDatabase() = default;
};
class MySQLDatabase : public IDatabase {
public:
void connect() override { std::cout << "MySQL connect" << std::endl; }
void disconnect() override { std::cout << "MySQL disconnect" << std::endl; }
void query(const std::string& sql) override {
std::cout << "MySQL query: " << sql << std::endl;
}
};
class PostgreSQLDatabase final : public IDatabase {
public:
void connect() override { std::cout << "PostgreSQL connect" << std::endl; }
void disconnect() override { std::cout << "PostgreSQL disconnect" << std::endl; }
void query(const std::string& sql) override final {
std::cout << "PostgreSQL query: " << sql << std::endl;
}
};
override and const
class Base {
public:
virtual void func() const {}
};
class Derived : public Base {
public:
void func() override {} // error: const mismatch
void func() const override {} // OK
};
Mistakes override and final turn into compile errors
Typos, signature drift, inheriting final classes, overriding final functions—override/final turn many of these into compile errors. Prefer composition over inheriting a final class when you need wrapping.
Combining override and final
class Base {
public:
virtual void func1() {}
virtual void func2() {}
};
class Middle : public Base {
public:
void func1() override {}
void func2() override final {}
};
class Leaf : public Middle {
public:
void func1() override {}
// void func2() override {} // error: final in Middle
};
Performance: vtables, devirtualization, measurement
- Typical virtual call: indirect branch via vtable—small cost, but can add up in hot loops.
final/finalclass: may enable direct calls or inlining when the implementation can prove the final callee (no UB).overrideitself: zero runtime cost—purely compile-time checking; it prevents wrong virtual calls. Practice: before addingfinalfor speed, profile to see if virtual calls are the bottleneck. Closing APIs has bigger design trade-offs than nanoseconds.
How the compiler uses final is easy to picture. Given Rectangle& r where Rectangle is final, a call r.area() can only ever reach Rectangle::area, because no more-derived type can exist. The compiler can emit a direct call and then inline it. Without final, it would need to know the dynamic type from context (for example, the object was constructed a few lines above), or use whole-program information: link-time optimization, or options such as Clang’s -fwhole-program-vtables or GCC’s -fdevirtualize-at-ltrans, which let the optimizer see every class in the program. Calls through a Shape&, which is the normal polymorphic case, are not devirtualized by marking one derived class final. The benefit is limited to code that already works with the concrete type.
The real cost of a virtual call is rarely the indirect branch itself, which modern CPUs predict well when the target is stable. It is the lost inlining: a tiny getter behind a virtual call cannot be folded into the caller’s loop, so the compiler cannot vectorize or hoist around it.
final on implementations and on template-method shells
Interface + final implementation
class IPaymentProcessor {
public:
virtual void processPayment(double amount) = 0;
virtual bool validateCard(const std::string& cardNumber) = 0;
virtual ~IPaymentProcessor() = default;
};
class SecurePaymentProcessor final : public IPaymentProcessor {
public:
void processPayment(double amount) override {
if (validateCard(currentCard_)) {
std::cout << "paid: $" << amount << '\n';
}
}
bool validateCard(const std::string& cardNumber) override final {
return cardNumber.length() == 16;
}
private:
std::string currentCard_;
};
Template method with final on the shell
class DataProcessor {
public:
virtual void process() final { // final requires virtual
loadData();
validateData();
transformData();
saveData();
}
virtual ~DataProcessor() = default;
protected:
virtual void loadData() = 0;
virtual void validateData() = 0;
virtual void transformData() = 0;
virtual void saveData() = 0;
};
In the classic template method pattern the outer process() is simply non-virtual, which already prevents overriding it. Making it virtual ... final only makes sense when the class itself derives from an interface that declares process() as virtual and you want to seal it at this level. If DataProcessor is the root of the hierarchy, prefer the plain non-virtual version: it avoids a vtable slot and states the intent just as clearly. Also keep the hooks protected (or private, which still allows overriding in C++) so callers cannot skip the fixed sequence by calling transformData() directly.
One pitfall specific to this pattern: do not call the virtual hooks from the base constructor or destructor. During DataProcessor’s constructor the derived part does not exist yet, so the call dispatches to DataProcessor’s own version, and for a pure virtual hook that is undefined behavior, typically reported at run time as “pure virtual method called” followed by std::terminate.
FAQ (selected)
Q: Always use override when overriding?
A: Yes—for every virtual override in new code.
Q: When to use final?
A: When extension is genuinely undesirable (security, invariants) or you have measured benefit—avoid locking APIs prematurely.
Q: Works without override?
A: Often yes at runtime—but override is strongly recommended.
Q: Performance upside of final?
A: Possible devirtualization/inlining—measure.
Q: override vs virtual in derived classes?
A: Base declares virtual; derived uses override. Writing virtual void f() override is legal but redundant, and the C++ Core Guidelines (C.128) recommend writing only one of virtual, override, or final.
Q: override and final together?
A: Yes: void f() override final.
Q: Downsides of final classes?
A: You cannot subclass later—only use when sure.
Resources: Scott Meyers Effective Modern C++ Item 12, cppreference override, cppreference final.
override verifies virtual overrides; final stops inheritance or further overrides.
Related Articles
- C++ Inheritance Design
- C++ Diamond Problem Guide
- C++ VTable | Virtual Function Table Guide
- C++ noexcept Specifier: Contracts, Moves, and std::terminate