Ensuring Correct Overrides
Use override to catch errors and final to prevent further overriding.
What Are the override and final Specifiers?
override and final are two words you can attach to a declaration to make the compiler check a claim you are making about an inheritance hierarchy. override claims that a member function replaces a virtual function it inherited. final claims that nobody further down the hierarchy may replace this function, or, when written after a class name, that nobody may derive from this class at all. Break either claim and the program stops compiling.
Neither word is a keyword. Both carry meaning only in the positions shown below, which is why a variable named final still compiles perfectly well elsewhere in your program. Strictly they are identifiers, though everyone, this lesson included, calls them specifiers.
| What you want the compiler to guarantee | What you write | Where it goes |
|---|---|---|
| This function replaces an inherited virtual function | override |
After the parameter list, following any const |
| No derived class may replace this function | final |
After the parameter list, following any const |
| No class may derive from this one | final |
Directly after the class name |
There is also one rule that these specifiers relax rather than enforce: an override is normally required to return the exact type the base version returns, and covariant return types are the documented exception. The last section covers it.
Why a Correct-Looking Override Can Do Nothing
Dynamic dispatch only reaches a derived function when that function is a genuine override, and the bar for "genuine" is exact. Name, parameter types, constness, and reference qualifiers must all agree with the inherited declaration. Anything else is simply a different function that happens to live in a derived class.
The export job below was supposed to give booklets a slower time estimate than the default. Somebody renamed the base class function and never touched the derived class, so the two names no longer line up:
#include <iostream>
class ExportJob
{
public:
virtual int estimateSeconds(int pageCount) { return pageCount * 3; }
};
class BookletExportJob : public ExportJob
{
public:
virtual int estimateSecs(int pageCount) { return pageCount * 11; } // meant to override, but the name no longer matches
};
int main()
{
BookletExportJob booklet{};
ExportJob& handle{ booklet };
std::cout << "estimate for 24 pages: " << handle.estimateSeconds(24) << " seconds" << '\n';
return 0;
}
estimate for 24 pages: 72 seconds
The intended answer was 264. handle refers to a BookletExportJob, so a reader skimming this code expects the booklet estimate. What actually happens is that BookletExportJob never overrode anything, ExportJob::estimateSeconds() is still the only implementation of that name, and the program quietly reports a figure that is off by a factor of nearly four. Nothing in the build output hints at it.
A failed override does not produce a broken program. It produces a working program that runs the wrong function. When the functions return values rather than printing labels, the only symptom is a number that is subtly wrong.
The Warning That Only Covers Half the Cases
There is one shape of this bug the compiler does say something about. When the derived function keeps the base name but changes the signature, the derived name hides the inherited one, and this platform's build flags report the hiding. Both functions below were meant to be overrides, and neither one is: the first changes the parameter type, the second adds const.
Broken on purpose. The two derived functions do not override anything:
#include <iostream>
class ExportJob
{
public:
virtual int memoryBudgetMb(int pageCount) { return pageCount * 2; }
virtual int workerThreads(int pageCount) { return pageCount / 8 + 1; }
};
class BookletExportJob : public ExportJob
{
public:
virtual int memoryBudgetMb(long pageCount) { return static_cast<int>(pageCount) * 5; } // parameter is a long
virtual int workerThreads(int pageCount) const { return pageCount / 4 + 1; } // function is const
};
int main()
{
BookletExportJob booklet{};
ExportJob& handle{ booklet };
std::cout << "budget: " << handle.memoryBudgetMb(24) << " MB" << '\n';
std::cout << "threads: " << handle.workerThreads(24) << '\n';
return 0;
}
s.cpp:7:17: warning: 'virtual int ExportJob::workerThreads(int)' was hidden [-Woverloaded-virtual=]
s.cpp:14:17: note: by 'virtual int BookletExportJob::workerThreads(int) const'
s.cpp:6:17: warning: 'virtual int ExportJob::memoryBudgetMb(int)' was hidden [-Woverloaded-virtual=]
s.cpp:13:17: note: by 'virtual int BookletExportJob::memoryBudgetMb(long int)'
budget: 48 MB
threads: 4
Two things are worth noticing. First, long instead of int is enough to break the match even though an int argument converts to long without complaint. Overriding compares declarations, not what a call could be made to work. Second, adding const breaks the match just as thoroughly, and that one is easy to write by accident because const on a member function feels like a detail rather than part of its identity.
The warning is real, but it is not a safety net. It fires because a name was hidden, so it says nothing at all about the renamed function from the previous section, and it is a warning rather than an error, so a build with a busy log will swallow it. What you want is a check you ask for explicitly and that fails the build.
Contract 1: This Function Must Replace an Inherited One
Writing override at the end of a member function declaration asks the compiler to prove the function is a genuine override. If it is not, compilation fails, and it fails at the derived class rather than at the call site far away.
Broken on purpose. All three declarations below carry override and none of them qualifies:
class ExportJob
{
public:
virtual int estimateSeconds(int pageCount) { return pageCount * 3; }
virtual int memoryBudgetMb(int pageCount) { return pageCount * 2; }
virtual int workerThreads(int pageCount) { return pageCount / 8 + 1; }
};
class BookletExportJob : public ExportJob
{
public:
int estimateSecs(int pageCount) override { return pageCount * 11; }
int memoryBudgetMb(long pageCount) override { return static_cast<int>(pageCount) * 5; }
int workerThreads(int pageCount) const override { return pageCount / 4 + 1; }
};
int main()
{
return 0;
}
s.cpp:12:9: error: 'int BookletExportJob::estimateSecs(int)' marked 'override', but does not override
s.cpp:13:9: error: 'int BookletExportJob::memoryBudgetMb(long int)' marked 'override', but does not override
s.cpp:14:9: error: 'int BookletExportJob::workerThreads(int) const' marked 'override', but does not override
Every failure the previous two sections could only hint at is now a named error on a numbered line, including the renamed function that produced no diagnostic at all before.
Three details govern how you write it:
- Position: it is written last, after the parameter list and after any trailing
const, which givesconst override. The reverse order,override const, is rejected. overrideimpliesvirtual, so a declaration never needs both. Writevirtualin the base class where the function is introduced, andoverridein every class that replaces it.- The check costs nothing at run time. It is a question answered while compiling, and the generated code is identical either way.
Mark a function
virtual in the class that introduces it, and mark it override in every class that replaces it. Never write both on the same declaration. This applies to virtual destructors as well as to ordinary member functions.
A function that is both
const and an override has to be written const override. Swapping the two words produces override const, which no compiler accepts.
With the signatures corrected, the same hierarchy dispatches the way the code always looked like it would:
#include <iostream>
class ExportJob
{
public:
virtual int estimateSeconds(int pageCount) { return pageCount * 3; }
virtual int memoryBudgetMb(int pageCount) const { return pageCount * 2; }
};
class BookletExportJob : public ExportJob
{
public:
int estimateSeconds(int pageCount) override { return pageCount * 11; }
int memoryBudgetMb(int pageCount) const override { return pageCount * 5; }
};
int main()
{
BookletExportJob booklet{};
ExportJob& handle{ booklet };
std::cout << "estimate for 24 pages: " << handle.estimateSeconds(24) << " seconds" << '\n';
std::cout << "budget for 24 pages: " << handle.memoryBudgetMb(24) << " MB" << '\n';
return 0;
}
estimate for 24 pages: 264 seconds
budget for 24 pages: 120 MB
Contract 2: No Further Overrides of This Function
final on a member function sits in the same position as override and makes the opposite promise: this implementation is the last one. Any class further down the hierarchy that tries to replace it is rejected.
Broken on purpose. SaddleStitchExportJob tries to override a function that was sealed one level up:
class ExportJob
{
public:
virtual int sheetsPerSignature() const { return 1; }
};
class BookletExportJob : public ExportJob
{
public:
int sheetsPerSignature() const override final { return 4; }
};
class SaddleStitchExportJob : public BookletExportJob
{
public:
int sheetsPerSignature() const override { return 8; }
};
int main()
{
return 0;
}
s.cpp:16:9: error: virtual function 'virtual int SaddleStitchExportJob::sheetsPerSignature() const' overriding final function
s.cpp:10:9: note: overridden function is 'virtual int BookletExportJob::sheetsPerSignature() const'
BookletExportJob::sheetsPerSignature() is a perfectly ordinary override of the base version; the error belongs entirely to the class below it. This is how you allow exactly one level of customisation and then close the door, which is useful when a further override could only break an invariant the middle class relies on.
Notice that the sealed function is written const override final. Both specifiers are doing separate work, and it is worth spelling out why final alone is not enough:
#include <iostream>
class ExportJob
{
public:
virtual int sheetsPerSignature() const { return 1; }
};
class BookletExportJob : public ExportJob
{
public:
virtual int sheetsPerSignatures() const final { return 4; }
};
int main()
{
BookletExportJob booklet{};
std::cout << booklet.sheetsPerSignatures() << '\n';
return 0;
}
4
That misspelled name compiles cleanly. final only asks whether the function may be overridden later, never whether it overrode anything earlier, so a sealed function that was meant to be an override can be just as silently wrong as the very first example in this lesson. override final asks both questions.
final on its own does not verify that a function overrides anything. Write override final whenever you intend both, and keep in mind that final on a member function that is not virtual at all is rejected outright.
Contract 3: No Derived Classes at All
Moving the same word to a different position changes the target from a function to the whole class. final written directly after a class name means no class may use it as a base.
Broken on purpose. BookletExportJob has been sealed, so the hierarchy cannot be extended:
class ExportJob
{
public:
virtual int sheetsPerSignature() const { return 1; }
};
class BookletExportJob final : public ExportJob
{
public:
int sheetsPerSignature() const override { return 4; }
};
class SaddleStitchExportJob : public BookletExportJob
{
public:
int sheetsPerSignature() const override { return 8; }
};
int main()
{
return 0;
}
s.cpp:13:7: error: cannot derive from 'final' base 'BookletExportJob' in derived type 'SaddleStitchExportJob'
Sealing the class is the blunter instrument. Marking every member function final would still leave the class open to being derived from and to having new state bolted onto it; final on the class itself rules the whole relationship out. BookletExportJob remains usable in every other way, and it still overrides ExportJob::sheetsPerSignature() as before.
The One Relaxation: Covariant Return Types
An override must otherwise return exactly what the base version returns. The single exception applies where the returned type is a class pointer or a class reference: the override is allowed to narrow it to any class that inherits from the one the base version names. Return types related that way are called covariant return types.
The two hierarchies below are separate. PageLayout and BookletLayout describe page geometry; ExportJob and BookletExportJob produce it. ExportJob::layout() promises a PageLayout*, and the override narrows that promise to a BookletLayout*, which is legal because BookletLayout derives from PageLayout:
#include <iostream>
class PageLayout
{
public:
explicit PageLayout(int marginMm) : m_marginMm{ marginMm } {}
int marginMm() const { return m_marginMm; }
private:
int m_marginMm{};
};
class BookletLayout : public PageLayout
{
public:
BookletLayout() : PageLayout{ 7 } {}
int gutterMm() const { return 9; }
};
class ExportJob
{
public:
virtual PageLayout* layout() { return &m_flat; }
private:
PageLayout m_flat{ 24 };
};
class BookletExportJob : public ExportJob
{
public:
BookletLayout* layout() override { return &m_folded; }
private:
BookletLayout m_folded{};
};
int main()
{
BookletExportJob booklet{};
std::cout << "margin: " << booklet.layout()->marginMm() << " mm" << '\n';
std::cout << "gutter: " << booklet.layout()->gutterMm() << " mm" << '\n';
ExportJob& handle{ booklet };
std::cout << "margin through the base handle: " << handle.layout()->marginMm() << " mm" << '\n';
ExportJob plain{};
std::cout << "margin of a plain job: " << plain.layout()->marginMm() << " mm" << '\n';
return 0;
}
margin: 7 mm
gutter: 9 mm
margin through the base handle: 7 mm
margin of a plain job: 24 mm
The third line is the one to study. handle refers to the same booklet object, layout() is virtual, so BookletExportJob::layout() runs and hands back the address of m_folded. The margin printed is 7, the booklet's own value, exactly as when the call went through booklet directly. Dynamic dispatch chose the function; covariance did not change that.
What covariance does change is the static type of the expression, and that is fixed by the type you called through, not by the object behind it. Through booklet the expression booklet.layout() has type BookletLayout*. Through handle the compiler only knows about ExportJob::layout(), so handle.layout() has type PageLayout* and the returned pointer is quietly converted up to it. The object is unchanged; only what the compiler will let you ask of it is narrower.
Broken on purpose. gutterMm() is a BookletLayout member, and through a base handle it is out of reach:
#include <iostream>
class PageLayout
{
public:
explicit PageLayout(int marginMm) : m_marginMm{ marginMm } {}
int marginMm() const { return m_marginMm; }
private:
int m_marginMm{};
};
class BookletLayout : public PageLayout
{
public:
BookletLayout() : PageLayout{ 7 } {}
int gutterMm() const { return 9; }
};
class ExportJob
{
public:
virtual PageLayout* layout() { return &m_flat; }
private:
PageLayout m_flat{ 24 };
};
class BookletExportJob : public ExportJob
{
public:
BookletLayout* layout() override { return &m_folded; }
private:
BookletLayout m_folded{};
};
int main()
{
BookletExportJob booklet{};
ExportJob& handle{ booklet };
std::cout << handle.layout()->gutterMm() << '\n';
return 0;
}
s.cpp:45:35: error: 'class PageLayout' has no member named 'gutterMm'
C++ never selects a type at run time, so this is the expected outcome rather than a limitation to work around: you get the derived return type only where the static type of the object you called through is the derived one.
Covariance is most often seen where a virtual function returns a pointer to the class holding it, so each level of a hierarchy hands back its own type. Nothing requires that arrangement. Any pair of return types will do, as long as the override's return type is a pointer or reference to a class derived from the one the base version returns.
Key Terminology
- Override: a derived class virtual function that replaces an inherited one, matching it in name, parameter list, constness, and reference qualifiers.
overridespecifier: a request that the compiler reject the declaration unless it really is an override.finalspecifier: on a virtual function, a bar on any further override; on a class name, a bar on any derivation.- Covariant return types: the exception that lets an override narrow a class pointer or class reference return type to a subclass of whatever the base version declared.
- Static type: the type an expression has according to its declaration, which is what decides which members are reachable and which return type covariance hands you.
Summary
| Situation | Diagnostic you get | Fix |
|---|---|---|
override on a function that matches nothing inherited |
marked 'override', but does not override |
Match the base declaration exactly, or drop override if a new function was intended |
A derived class overrides a function marked final |
overriding final function |
Remove final from the base version, or stop overriding it |
A class derives from a class marked final |
cannot derive from 'final' base |
Remove final from the base class, or compose instead of inheriting |
final on a member function that is not virtual |
marked 'final', but is not virtual |
Make the function virtual, or drop final |
| No specifier and a mismatched signature | Silence, or at best a hidden-name warning | Add override so the mismatch becomes an error |
- An override must match the inherited declaration exactly. A different parameter type, an added
const, or a renamed function all produce a separate function instead, and calls through a base reference keep reaching the base version. overrideturns that silent mismatch into a compile error, impliesvirtual, and costs nothing at run time, so every replacing function should carry it.- Word order matters when both apply:
const override, never the reverse. finalon a member function forbids further overrides;finalafter a class name forbids derivation.finalnever checks that the function overrode anything, so pair it withoverridewhen both properties are intended.- Covariant return types let an override narrow a class pointer or class reference return type to a subclass. The function that runs is chosen by dynamic dispatch; the return type you get is chosen by the static type of the object you called through.
How did you find this lesson?
Your rating helps us improve the content.
Create an account to track your progress and access interactive exercises. Already have one? Sign in.
Ensuring Correct Overrides - Quiz
Test your understanding of the lesson.
Practice Exercises
Override and Final Specifiers
Practice using the override and final specifiers to ensure correct function overriding. Learn how these specifiers help catch errors at compile-time.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!