C++ / INHERITANCE AND POLYMORPHISM
Override, final, and catching signature mistakes
Use override to turn near-miss virtual declarations into compile errors, and final to seal a function or a class against further derivation.
What you will learn
- Add override to every derived virtual declaration so a signature typo fails to compile
- Recognise that a const or parameter mismatch creates a hiding function, not an override
- Use final on a function to close it and on a class to forbid any derivation
- Know that covariant pointer or reference returns are the only allowed return-type change
Understanding Override, final, and catching signature mistakes
A derived declaration overrides a base virtual function only when the name, the parameter type list, the const/volatile qualification and the ref-qualifier all match exactly. When one of those drifts, the compiler cannot tell a typo from a deliberately different function, so it takes the declaration at face value: you get a new member that hides the base name inside the derived class, while the base's virtual dispatch entry still points at the base implementation. That is why the bug is so hard to see. Calling on a derived-typed object finds the new function and looks correct, but any call through a base reference or pointer runs the base version.
The override specifier inverts that default by asserting, at compile time, that this declaration really does replace a virtual function found in some base. The compiler checks the assertion and rejects the program when nothing matches, which catches a forgotten const, an int where the base said long, a misspelled name, a base function that was never declared virtual, and, most valuably, a later edit to the base signature that would otherwise quietly orphan every derived class. Writing override also makes the function virtual, so a derived declaration needs only one of the two words, and the convention is to keep override.
The final specifier closes doors instead of checking them. On a virtual function it forbids any further derived class from overriding it; on a class it forbids using that class as a base at all. Both state that a hierarchy stops here and have the compiler enforce it instead of a comment, and as a side effect they let the optimizer turn a virtual call into a direct one whenever the static type is known to be final. Neither specifier changes the behaviour of a program that was already correct; they only change which programs are allowed to compile.
<iostream>
<string>
struct Shape {
virtual std::string name() const { return "Shape"; }
virtual ~Shape() = default;
};
struct Circle : Shape {
// No trailing const: this is a NEW function, not an override.
std::string name() { return "Circle"; }
// std::string name() override { return "Circle"; } // error: does not override
};
struct Square : Shape {
std::string name() const override { return "Square"; }
};
void report(const Shape& s) {
std::cout << "report sees: " << s.name() << '\n';
}
int main() {
Circle c;
Square q;
std::cout << "direct call: " << c.name() << '\n';
report(c);
report(q);
}
An override is matched by exact signature, so override and final exist to make the compiler reject a near-miss declaration that would otherwise compile as a silent new function.
Worked examples
A parameter mismatch hides every base overload
A derived function whose parameter type is merely close to the base's takes over name lookup while overriding nothing.
<iostream>
struct Logger {
virtual void write(int code) { std::cout << "Logger::write(int) " << code << '\n'; }
virtual void write(double value) { std::cout << "Logger::write(double) " << value << '\n'; }
virtual ~Logger() = default;
};
struct FileLogger : Logger {
// long, not int: matches neither base signature
void write(long code) { std::cout << "FileLogger::write(long) " << code << '\n'; }
};
int main() {
FileLogger f;
f.write(1);
f.write(2.5);
Logger& base = f;
base.write(1);
base.write(2.5);
}
Example explained
Line 1write(long) overrides nothing, so name lookup inside FileLogger finds it and stops, hiding both base overloads.
Line 2f.write(2.5) therefore has only one candidate, and the double is converted to long, so 2.5 silently becomes 2.
Line 3Through Logger& the call uses the dispatch entries FileLogger never filled, so both base versions run instead.
Line 4Appending override to write(long) turns this into a compile error; adding using Logger::write; brings the base overloads back into scope.
Sealing a function and sealing a class
final on a member function stops further overriding, and final on a class stops derivation, without changing how dispatch works.
<iostream>
struct Widget {
virtual void draw() const { std::cout << "Widget\n"; }
virtual ~Widget() = default;
};
struct Button : Widget {
void draw() const final { std::cout << "Button\n"; }
};
struct IconButton final : Button {
// void draw() const override {} // error: overrides final Button::draw
void setIcon() { std::cout << "icon set\n"; }
};
// struct Fancy : IconButton {}; // error: IconButton is final
int main() {
IconButton b;
b.setIcon();
const Widget& w = b;
w.draw();
}
Example explained
Line 1void draw() const final both overrides Widget::draw and forbids anything below Button from replacing it.
Line 2IconButton inherits Button::draw unchanged, which is why the call through const Widget& prints Button.
Line 3final after the class name in struct IconButton final : Button rejects any attempt to derive from it, so the commented-out Fancy will not compile.
Line 4w.draw() is still a virtual call; final only shrinks the set of possible targets, it does not disable dispatch.
The one return type you are allowed to change
Covariant pointer returns are legal in an override, while any other return-type difference is rejected even without the override specifier.
<iostream>
<memory>
struct Node {
virtual Node* clone() const { return new Node(*this); }
virtual const char* tag() const { return "Node"; }
virtual ~Node() = default;
};
struct Leaf : Node {
Leaf* clone() const override { return new Leaf(*this); } // covariant return: allowed
const char* tag() const override { return "Leaf"; }
// int tag() const override; // error: conflicting return type
};
int main() {
Leaf leaf;
std::unique_ptr<Leaf> exact{leaf.clone()};
const Node& n = leaf;
std::unique_ptr<Node> viaBase{n.clone()};
std::cout << exact->tag() << ' ' << viaBase->tag() << '\n';
}
Example explained
Line 1Leaf* clone() const override is accepted because Leaf derives from Node and the pointed-to cv-qualification matches, which is what covariant means.
Line 2The commented int tag() const override fails because return types are not part of the matching rule, so a mismatch there is a conflicting declaration rather than a new function.
Line 3That asymmetry is worth remembering: the compiler already catches return-type slips, but const and parameter slips need override to be caught.
Line 4n.clone() dispatches to Leaf::clone at run time and only narrows the static type to Node*, so viaBase->tag() still prints Leaf.
Important notes
override and final are identifiers with special meaning, not reserved words; they only act as specifiers at the end of a declarator, so older code that uses final as a variable name still compiles.
The match also covers ref-qualifiers and exception specifications: void f() & does not override void f(), and an override of a noexcept base function must itself be noexcept, which is a hard error rather than silent hiding.
Common mistakes
Dropping the trailing const in the derived declaration: the code compiles, derived-typed calls print the derived result, and every call through a base reference silently runs the base version.
Writing virtual void f() final in a derived class: if the signature has drifted you get a brand-new sealed virtual function, whereas final on its own would have failed because only a virtual function can be marked final.
Repeating override on the out-of-class definition, as in std::string Circle::name() const override { ... }: that is a compile error, since the specifier belongs only on the in-class declaration.
Try it yourself
Change, predict, then run
Write a Base with virtual double area() const returning 1.0 and a Derived declaring double area() without const returning 2.0, then print the value through a const Base& and confirm it shows 1. Add override to the derived declaration, watch it fail to compile, restore the const, and check that the printed value becomes 2.
Open the C++ workspaceCheck your understanding
A base class declares virtual void render(int layer) const, and a derived class declares void render(int layer) with no const and no override. What actually happens?
- It compiles; calls through a base reference run the base version, and the derived declaration hides the base name for derived-typed calls
- It compiles and overrides the base function, because const qualification is ignored when matching virtual functions
- It fails to compile, because a derived class may not declare a function with the same name as a base virtual function
- It compiles and overrides the base function, but the override is only used when the object is not const
Show answer
The const qualification is part of the signature used to match overrides, so the derived declaration is a different function: the base's dispatch entry is untouched and a call through const Base& lands on the base implementation, while name lookup inside the derived class finds the new declaration first. Option 4 is tempting because the derived version does run when you call it on a non-const derived object, but that is ordinary name hiding rather than dynamic dispatch, and a base reference bound to that same object still calls the base version.