The Override and Final Specifiers, and Covariant Return Types

To address some common challenges with inheritance, C++ has two inheritance-related identifiers: override and final. Note that these identifiers are not keywords -- they are normal words that have special meaning only when used in certain contexts. The C++ standard calls them "identifiers with special meaning", but they are often referred to as "specifiers".

Although final isn't used very much, override is a fantastic addition that you should use regularly. In this lesson, we'll take a look at both, as well as one exception to the rule that virtual function override return types must match.

The override specifier

As we mentioned in the previous lesson, a derived class virtual function is only considered an override if its signature and return types match exactly. That can lead to inadvertent issues, where a function that was intended to be an override actually isn't.

Consider the following example:

#include <iostream>
#include <string_view>

class Validator
{
public:
	virtual std::string_view check(int value) { return "Validator"; }
	virtual std::string_view verify(int value) { return "Validator"; }
};

class EmailValidator : public Validator
{
public:
	virtual std::string_view check(short value) { return "EmailValidator"; } // note: parameter is a short
	virtual std::string_view verify(int value) const { return "EmailValidator"; } // note: function is const
};

int main()
{
	EmailValidator ev{};
	Validator& rBase{ ev };
	std::cout << rBase.check(1) << '\n';
	std::cout << rBase.verify(2) << '\n';

	return 0;
}

Because rBase is a Validator reference to an EmailValidator object, the intention here is to use virtual functions to access EmailValidator::check() and EmailValidator::verify(). However, because EmailValidator::check() takes a different parameter (a short instead of an int), it's not considered an override of Validator::check(). More insidiously, because EmailValidator::verify() is const and Validator::verify() isn't, EmailValidator::verify() isn't considered an override of Validator::verify().

Consequently, this program prints:

Validator Validator

In this particular case, because Validator and EmailValidator just print their names, it's fairly easy to see that we messed up our overrides, and that the wrong virtual function is being called. However, in a more complicated program, where the functions have behaviors or return values that aren't printed, such issues can be very difficult to debug.

To help address the issue of functions that are meant to be overrides but aren't, the override specifier can be applied to any virtual function to tell the compiler to enforce that the function is an override. The override specifier is placed at the end of a member function declaration (in the same place where a function-level const goes). If a member function is const and an override, the const must come before override.

If a function marked as override does not override a base class function (or is applied to a non-virtual function), the compiler will flag the function as an error.

#include <string_view>

class Validator
{
public:
	virtual std::string_view check(int value) { return "Validator"; }
	virtual std::string_view verify(int value) { return "Validator"; }
	virtual std::string_view validate(int value) { return "Validator"; }
};

class EmailValidator : public Validator
{
public:
	std::string_view check(short int value) override { return "EmailValidator"; } // compile error, function is not an override
	std::string_view verify(int value) const override { return "EmailValidator"; } // compile error, function is not an override
	std::string_view validate(int value) override { return "EmailValidator"; } // okay, function is an override of Validator::validate(int)

};

int main()
{
	return 0;
}

The above program produces two compile errors: one for EmailValidator::check(), and one for EmailValidator::verify(), because neither override a prior function. EmailValidator::validate() does override Validator::validate(), so no error is produced for that line.

Because there is no performance penalty for using the override specifier and it helps ensure you've actually overridden the function you think you have, all virtual override functions should be tagged using the override specifier. Additionally, because the override specifier implies virtual, there's no need to tag functions using the override specifier with the virtual keyword.

Best Practice
Use the virtual keyword on virtual functions in a base class. Use the override specifier (but not the virtual keyword) on override functions in derived classes. This includes virtual destructors.
Rule
If a member function is both const and an override, the const must be listed first. const override is correct, override const is not.

The final specifier

There may be cases where you don't want someone to be able to override a virtual function, or inherit from a class. The final specifier can be used to tell the compiler to enforce this. If the user tries to override a function or inherit from a class that has been specified as final, the compiler will give a compile error.

In the case where we want to restrict the user from overriding a function, the final specifier is used in the same place the override specifier is, like so:

#include <string_view>

class Validator
{
public:
	virtual std::string_view check() const { return "Validator"; }
};

class EmailValidator : public Validator
{
public:
	// note use of final specifier on following line -- that makes this function not able to be overridden in derived classes
	std::string_view check() const override final { return "EmailValidator"; } // okay, overrides Validator::check()
};

class StrictEmailValidator : public EmailValidator
{
public:
	std::string_view check() const override { return "StrictEmailValidator"; } // compile error: overrides EmailValidator::check(), which is final
};

In the above code, EmailValidator::check() overrides Validator::check(), which is fine. But EmailValidator::check() has the final specifier, which means that any further overrides of that function should be considered an error. And indeed, StrictEmailValidator::check() tries to override EmailValidator::check() (the override specifier here isn't relevant, it's just there for good practice), so the compiler will give a compile error.

In the case where we want to prevent inheriting from a class, the final specifier is applied after the class name:

#include <string_view>

class Validator
{
public:
	virtual std::string_view check() const { return "Validator"; }
};

class EmailValidator final : public Validator // note use of final specifier here
{
public:
	std::string_view check() const override { return "EmailValidator"; }
};

class StrictEmailValidator : public EmailValidator // compile error: cannot inherit from final class
{
public:
	std::string_view check() const override { return "StrictEmailValidator"; }
};

In the above example, class EmailValidator is declared final. Thus, when StrictEmailValidator tries to inherit from EmailValidator, the compiler will give a compile error.

Covariant return types

There is one special case in which a derived class virtual function override can have a different return type than the base class and still be considered a matching override. If the return type of a virtual function is a pointer or a reference to some class, override functions can return a pointer or a reference to a derived class. These are called covariant return types. Here is an example:

#include <iostream>
#include <string_view>

class Processor
{
public:
	// This version of getThis() returns a pointer to a Processor class
	virtual Processor* getThis() { std::cout << "called Processor::getThis()\n"; return this; }
	void printType() { std::cout << "returned a Processor\n"; }
};

class GraphicsProcessor : public Processor
{
public:
	// Normally override functions have to return objects of the same type as the base function
	// However, because GraphicsProcessor is derived from Processor, it's okay to return GraphicsProcessor* instead of Processor*
	GraphicsProcessor* getThis() override { std::cout << "called GraphicsProcessor::getThis()\n";  return this; }
	void printType() { std::cout << "returned a GraphicsProcessor\n"; }
};

int main()
{
	GraphicsProcessor gp{};
	Processor* p{ &gp };
	gp.getThis()->printType(); // calls GraphicsProcessor::getThis(), returns a GraphicsProcessor*, calls GraphicsProcessor::printType
	p->getThis()->printType(); // calls GraphicsProcessor::getThis(), returns a Processor*, calls Processor::printType

	return 0;
}

This prints:

called GraphicsProcessor::getThis() returned a GraphicsProcessor called GraphicsProcessor::getThis() returned a Processor

One interesting note about covariant return types: C++ can't dynamically select types, so you'll always get the type that matches the actual version of the function being called.

In the above example, we first call gp.getThis(). Since gp is a GraphicsProcessor, this calls GraphicsProcessor::getThis(), which returns a GraphicsProcessor*. This GraphicsProcessor* is then used to call non-virtual function GraphicsProcessor::printType().

Now the interesting case. We then call p->getThis(). Variable p is a Processor pointer to a GraphicsProcessor object. Processor::getThis() is a virtual function, so this calls GraphicsProcessor::getThis(). Although GraphicsProcessor::getThis() returns a GraphicsProcessor*, because Processor version of the function returns a Processor*, the returned GraphicsProcessor* is upcast to a Processor*. Because Processor::printType() is non-virtual, Processor::printType() is called.

In other words, in the above example, you only get a GraphicsProcessor* if you call getThis() with an object that is typed as a GraphicsProcessor object in the first place.

Note that if printType() were virtual instead of non-virtual, the result of p->getThis() (an object of type Processor*) would have undergone virtual function resolution, and GraphicsProcessor::printType() would have been called.

Covariant return types are often used in cases where a virtual member function returns a pointer or reference to the class containing the member function (e.g. Processor::getThis() returns a Processor*, and GraphicsProcessor::getThis() returns a GraphicsProcessor*). However, this isn't strictly necessary. Covariant return types can be used in any case where the return type of the override member function is derived from the return type of the base virtual member function.

Summary

The override specifier: The override specifier is placed at the end of a member function declaration to tell the compiler that the function must override a base class virtual function. If the function doesn't override (due to signature mismatch, missing virtual in base, or other issues), the compiler produces an error, catching potential bugs at compile time.

Benefits of override: Using override prevents inadvertent errors where a function intended to be an override isn't actually overriding due to subtle signature differences. It provides compile-time safety with no runtime performance penalty. All virtual override functions should use the override specifier for clarity and error prevention.

The final specifier on functions: When applied to a virtual function, final prevents any further overrides of that function in classes derived from the current class. Attempting to override a final function produces a compile error. This is useful when you want to allow one level of overriding but prevent further customization down the inheritance chain.

The final specifier on classes: When applied to a class name (after the class name in the declaration), final prevents any classes from inheriting from that class. This is useful when you want to ensure a class cannot be used as a base class, preventing unintended inheritance relationships.

Covariant return types: An exception to the rule that override return types must match the base function. If the base virtual function returns a pointer or reference to a class, the override can return a pointer or reference to a derived class. C++ cannot dynamically select types, so the actual type returned matches the version of the function being called.

Usage with const: When a function is both const and an override, the const keyword must come before override in the declaration. The correct syntax is const override, not override const.

The override and final specifiers provide important compile-time safety mechanisms for inheritance hierarchies, helping catch errors and express design intent explicitly in code.