Covariant Return Type in C++
For the most part, the reason you override a member function is to change its implementation. Sometimes, however, you may want to change other characteristics of the member function, such as its return type.
A good rule of thumb is to override a member function with the exact member function declaration, or member function prototype, that the base class uses. The implementation can change, but the prototype stays the same.
That does not have to be the case, however. In C++, an overriding member function can change the return type as long as (1) the return type of the member function in the base class is a pointer or reference to a class, and (2) the return type in the derived class is a pointer or reference to a descendant, i.e., more specialized class.
Such types are called covariant return types.
This feature sometimes comes in handy when the base class and derived class work with objects in a parallel hierarchy—that is, another group of classes that is tangential, but related, to the first class hierarchy.
For example, consider a basic car simulator. You might have two hierarchies of classes that model different real-world objects but are obviously related. The first is the Car
hierarchy. The base class, Car
, has derived classes GasolineCar
and ElectricalCar
. Similarly, there is another hierarchy of classes with a base class called PowerSource
and derived classes GasolinePowerSource
and ElectricalPowerSource
.
Let's assume a power source can print its own type and that a gasoline power source has a member function fillTank(), while an electrical power source has a member function chargeBatteries():
class PowerSource { public: virtual void printType() = 0; }; class GasolinePowerSource : public PowerSource { public: void printType() override { println("GasolinePowerSource"); } virtual void fillTank() { println("Gasoline tank filled up."); } }; class ElectricalPowerSource : public PowerSource { public: void printType() override { println("ElectricalPowerSource"); } virtual void chargeBatteries() { println("Batteries charged."); } };
Now assume that Car
has a virtual member function called getFilledUpPowerSource()
that returns a reference to the filled-up
power source of a specific car:
class Car { public: virtual PowerSource& getFilledUpPowerSource() = 0; };
This is a pure virtual, abstract member function, as it only makes sense to provide an actual implementation in concrete derived classes. Since a GasolinePowerSource
is a PowerSource
, the GasolineCar
class can implement this member function as follows:
class GasolineCar : public Car { public: PowerSource& getFilledUpPowerSource() override { m_engine.fillTank(); return m_engine; } private: GasolinePowerSource m_engine; };
ElectricalCar
can implement it analogously as follows:
class ElectricalCar : public Car { public: PowerSource& getFilledUpPowerSource() override { m_engine.chargeBatteries(); return m_engine; } private: ElectricalPowerSource m_engine; };
This implementation is fine. However, because you know that the getFilledUpPowerSource()
member function for GasolineCar
always returns a GasolinePowerSource
, and for ElectricalCar
always an ElectricalPowerSource
, you can indicate this fact to potential users of these classes by changing the return type, as shown here:
class GasolineCar : public Car { public: GasolinePowerSource& getFilledUpPowerSource() override { /* omitted for brevity */} }; class ElectricalCar : public Car { public: ElectricalPowerSource& getFilledUpPowerSource() override { /* omitted for brevity */} };
A good way to figure out whether you can change the return type of an overridden member function is to consider whether existing code would still work; this is called the Liskov substitution principle (LSP). In the preceding example, changing the return type was fine because any code that assumed that the getFilledUpPowerSource()
member function would always return a PowerSource
would still compile and work correctly. Because an ElectricalPowerSource
and a GasolinePowerSource
are both PowerSource
's, any member functions that were called on the result of getFilledUpPowerSource()
returning a PowerSource
could still be called on the result of getFilledUpPowerSource()
returning an ElectricalPowerSource
or a GasolinePowerSource
.