Contact
Evil Genius SocietyHomeLearnForumLeaderboardSocialsMessages
← Back to module

Lesson 5 of 9

Lesson 16.2 mentioned override in passing. It deserves a lesson, because the bug it prevents is invisible: a function that looks like an override, compiles cleanly, and is never called.

What they do

#include <iostream>
#include <memory>
#include <string>
#include <vector>

class Handler {
public:
    virtual ~Handler() = default;
    virtual void process(const std::string& msg) const = 0;
    virtual int priority() const { return 0; }
};

class Logger : public Handler {
public:
    void process(const std::string& msg) const override {
        std::cout << "  [log] " << msg << "\n";
    }
    int priority() const override { return 5; }
};

// `final` on a class: nothing may derive from it.
class Alerter final : public Handler {
public:
    void process(const std::string& msg) const override {
        std::cout << "  [ALERT] " << msg << "\n";
    }
    // `final` on a function: no further class may override it.
    int priority() const final { return 10; }
};

int main() {
    std::vector<std::unique_ptr<Handler>> handlers;
    handlers.push_back(std::make_unique<Logger>());
    handlers.push_back(std::make_unique<Alerter>());

    for (const auto& h : handlers) {
        std::cout << "priority " << h->priority() << ":\n";
        h->process("disk usage at 91%");
    }
    return 0;
}
priority 5:
  [log] disk usage at 91%
priority 10:
  [ALERT] disk usage at 91%

override says "this is meant to replace a base virtual function". If it does not, the compiler rejects it.

final says "no further derivation". On a class, nothing may inherit from it. On a virtual function, no further class may override that function.

The bug override prevents

// struct Base {
//     virtual ~Base() = default;
//     virtual void f(int) const {}
//     virtual void g() const {}
// };
//
// struct A : Base { void f(double) const override {} };   // wrong parameter type
// struct B : Base { void g() {} };                        // missing const, no override
// struct Sealed final : Base { void g() const final {} };
// struct C : Sealed { };                                  // cannot derive from final

Three different diagnostics:

error: 'void A::f(double) const' marked 'override', but does not override
warning: 'virtual void Base::f(int) const' was hidden [-Woverloaded-virtual=]
warning: 'virtual void Base::g() const' was hidden [-Woverloaded-virtual=]
error: cannot derive from 'final' base 'Sealed' in derived type 'C'

Compare the two failures carefully, because the difference is the whole argument.

A::f(double) is marked override and is therefore an error. The build stops and names the function.

B::g() forgot const and was not marked override, so it is only a warning — and -Woverloaded-virtual is not in -Wall or -Wextra by default. In a normal build this compiles silently, B gets a brand-new non-virtual g(), and every call through a Base* keeps running Base::g. The program is wrong and nothing said so.

That is the case override exists for. Write it on every overriding function, without exception. It costs one word and converts the module's most common silent bug into a compile error.

The signature has to match exactly: parameter types, const, ref-qualifiers, and return type (except for covariant returns). Every one of those is a chance to get it subtly wrong.

final and the performance claim

final is usually justified two ways: as documentation of design intent, and as an enabler of devirtualization — the compiler proving which function will run and inlining it.

The first is real. The second did not survive measurement:

#include <chrono>
#include <iostream>
#include <random>
#include <vector>

template <typename F>
long long time_us(F f) {
    auto t0 = std::chrono::steady_clock::now();
    f();
    auto t1 = std::chrono::steady_clock::now();
    return std::chrono::duration_cast<std::chrono::microseconds>(t1 - t0).count();
}

struct Base { virtual ~Base() = default; virtual int f(int x) const = 0; };
struct Open  : Base { int f(int x) const override { return x * 2; } };
struct Sealed final : Base { int f(int x) const override { return x * 2; } };

int main() {
    const int N = 20000000;
    std::vector<int> data(N);
    std::mt19937 gen(42);
    for (int& v : data) v = static_cast<int>(gen() % 1000);

    Open open;
    Sealed sealed;
    long long sink = 0;

    // The static type is known here, so `final` lets the compiler prove
    // which function will run and inline it.
    auto t_open = time_us([&] {
        long long s = 0;
        for (int v : data) s += open.f(v);
        sink += s;
    });
    auto t_sealed = time_us([&] {
        long long s = 0;
        for (int v : data) s += sealed.f(v);
        sink += s;
    });

    // Through a base reference, neither can be devirtualised.
    const Base& ref_open = open;
    const Base& ref_sealed = sealed;
    auto t_ref_open = time_us([&] {
        long long s = 0;
        for (int v : data) s += ref_open.f(v);
        sink += s;
    });
    auto t_ref_sealed = time_us([&] {
        long long s = 0;
        for (int v : data) s += ref_sealed.f(v);
        sink += s;
    });

    std::cout << "through the concrete type:\n";
    std::cout << "  non-final: " << t_open << " us\n";
    std::cout << "  final:     " << t_sealed << " us\n";
    std::cout << "through a Base&:\n";
    std::cout << "  non-final: " << t_ref_open << " us\n";
    std::cout << "  final:     " << t_ref_sealed << " us\n";
    std::cout << "(sink " << sink << ")\n";
    return 0;
}

Three consecutive runs, compiled -O2:

through the concrete type:      non-final 7709 us    final 7389 us
through a Base&:                non-final 19891 us   final 25805 us

through the concrete type:      non-final 7522 us    final 7503 us
through a Base&:                non-final 19857 us   final 25643 us

through the concrete type:      non-final 7582 us    final 7501 us
through a Base&:                non-final 20179 us   final 25674 us

final produced no benefit in either configuration.

Through the concrete type both versions took about 7.5 ms, because the compiler already knew the exact type — open.f(v) has a static type of Open, so there was nothing left for final to prove. Devirtualization happened with or without it.

Through a Base& both were slower, and the final version was consistently slower still — 25.7 ms against 20.0 ms, reproducibly. That is the wrong direction for a supposed optimisation, which is the signal that it is a code-layout artifact rather than a dispatch effect. A const Base& could refer to an Open just as easily as a Sealed; marking one derived class final tells the compiler nothing about what the reference points at.

So the honest position:

Use final to express design intent, not to make code faster. Devirtualization through final requires the compiler to know the static type is the final class — and when it knows that, it usually did not need final anyway.

The cases where final genuinely helps are whole-program optimisation and link-time optimisation, where the compiler can see there is only one implementation in the entire program. That is a different mechanism, and it is not something a microbenchmark in one translation unit will show.

When to use final

On a class, when deriving from it would break an invariant. A class with a non-virtual destructor that is nonetheless meant to be copied around is a good candidate — final makes the dangerous inheritance impossible rather than merely unwise.

On a function, when further overriding would break the class's contract. A Handler whose priority() is fixed by regulation, say.

Not by default. final removes the ability of future code to extend yours. That is sometimes correct and sometimes a decision you will regret in two years. Unlike override, it is not a free win.

Covariant return types

The one signature difference override permits:

// struct Shape {
//     virtual Shape* clone() const = 0;
// };
//
// struct Circle : Shape {
//     Circle* clone() const override { return new Circle(*this); }
//     //  ^^^^^^ narrower return type — legal, and useful
// };
//
// Circle* c = /* ... */;
// Circle* copy = c->clone();      // no cast needed

The return type may be a pointer or reference to a class derived from the base's return type. This is why clone() idioms work without casting.

It does not extend to smart pointers: std::unique_ptr<Circle> is not derived from std::unique_ptr<Shape>, so a covariant clone returning unique_ptr does not compile. The usual workaround is a private virtual returning a raw pointer, wrapped by a public non-virtual returning unique_ptr.

Try it

Write a base class with four virtual functions and deliberately break the override contract four different ways in derived classes — wrong parameter type, wrong constness, wrong return type, and a typo in the name. Add override to each and record what the compiler says. Then mark one class final and try to derive from it.

Checkpoint

  • Write override on every overriding function. It is one word and it catches silent bugs.
  • A mismatched signature without override creates a new function; calls keep using the base.
  • The const mismatch is the common one, and -Woverloaded-virtual is not in -Wall.
  • A conflicting return type is an error with or without override.
  • final on a class forbids derivation; on a function, forbids further overriding.
  • Measured: final gave no devirtualization benefit in a single translation unit, and ran consistently slower through a base reference — a layout artifact, not a dispatch effect.
  • Use final for design intent; devirtualization needs the static type to be the final class.
  • final removes future extensibility, so it is not a free win the way override is.
  • Covariant returns let an override return a more derived pointer or reference.
  • Covariance does not work through std::unique_ptr; wrap a raw-pointer virtual instead.

Questions about this lesson

Log in to ask a question.