In this post, I want to talk about a specific feature. To set the table, it has to do with the return type of virtual functions. The compiler usually enforces that the return type of an override method match exactly the type of the base method declaration. However, there is a little leeway, with covariant return types. This is an interesting feature in C++ which isn’t supported in C#. In a nutshell, it allows the overriding method to change the return type, as long as it is a covariant return type. For example:
class Y {}; class Z : public Y {}; struct A { virtual Y* foo() { return new Y(); } }; struct B : public A { Z* foo() { return new Z(); } // legal code. Notice the // change of return type. };
This follows the Liskov Substitution Principle (LSP) very nicely, since class Z IS A class A and can be substituted as such. The standard explains this in section [class.virtual / 10.3 ] paragraph 7. This is where the conditions placed on using covariant return types are outlined:
Condition 1: both are pointers to classes, both are lvalue references to classes, or both are rvalue references to classes. For example, returning covariant types by value is not allowed:
class Y {}; class Z : public Y {}; struct A { virtual Y foo() { return Y(); } }; struct B : public A { Z foo() { return Z(); } // NOT legal code };
Condition 2: the class in the return type of B::f is the same class as the class in the return type of D::f, or is an unambiguous and accessible direct or indirect base class of the class in the return type of D::f. The following example is not allowed since the base class Y is not accessible since Z privately inherits from Y:
class Y {}; class Z : private Y {}; // Base class Y is // privately inherited. struct A { virtual Y foo() { return Y(); } }; struct B : public A { Z foo() { return Z(); } // NOT legal code };
Condition 3: both pointers or references have the same cv-qualification and the class type in the return type of D::f has the same cv-qualification as or less cv-qualification than the class type in the return type of B::f.
class X {}; class Y : public X {}; class Z : public Y {}; struct A { virtual Y* foo() { return new Y(); } virtual const Y* bar() { return new Y(); } }; struct B : public A { const Z* foo() // NOT legal code: { return new Z(); } // return type is more cv-qualified. virtual Z* bar() // legal code: { return new Z(); } // return type is less cv-qualified. };
The last interesting piece is this “When the overriding function is called as the final overrider of the overridden function, its result is converted to the type returned by the (statically chosen) overridden function (5.2.2).”
class Y {}; class Z : public Y {}; struct A { virtual Y* foo() { return new Y(); } }; struct B : public A { Z* foo() { return new Z(); } // legal code. // Notice the change of return type. }; A* pA = new B(); B* pB = new B(); Y* pY = pA->foo(); // Calls B::foo, returns Y*. Z* pY = pB->foo(); // Calls B::foo, returns Z*. Z* pZ = pA->foo(); // ERROR: Calls B::foo, returns Y* // since the static type is class A.
Final Thoughts: A few rules to keep in mind but overall covariant return types are handy feature if you find yourself needing to implement a concrete factory. Especially if you ever find your compiler’s implementation of dynamic_cast isn’t that efficient.