HOWTO: Covariant Return Types in C++

20060406T0140


Covariant return types are a useful feature of the C++ language that could save you from doing some extra, unnecessary work in a few com mon situations. They can also make the intended purpose of your functions more clear. This HOWTO explains what they are, how to use them, a nd gives some common sample applications.

Consider a simple inheritance hierarchy such as that shown here:

  Base
   |
   v
Derived

Often, the derived class has a polymorphic function which returns a more specific (i.e. more derived) type than the base class. This is per fectly legal, since the pointer is automatically downcast to the base class type.

A common use of this (but by no means the only one) is a clone() method:

class Base {
public:
    virtual Base* clone() const
    {
        return new Base(this);
    }
};

class Derived : public Base {
public:
    virtual Base* clone() const
    {
        return new Derived(this);
    }
};

However, now consider a bit of code that knows it specifically has a Derived instance, and wishes to clone it. This code must retrieve a base class pointer and then dynamically upcast it. We could assume that the upcast always succeeds and not perform check, but th is is bad practice (what if somebody changes the underlying function, or somebody copies and pastes the dynamic cast line?).

This leads to the following code:

    Derived* d = new Derived;
    Base* b = d->clone();
    Derived* d2 = dynamic_cast<Derived*>(b);
    if(!d2) {
        delete b;
        throw SomeCrazyException();
    }

As you can see, this is pretty long-winded given that we _know_ d2 should be a Derived instance.

If, instead, the clone() method could tell the compiler "actually, I always return a Derived*" (i.e. it became virtual Derived* Derived::clone() we could do away with the upcast and the check, moving all this away from run-time and into compile-time:

    Derived* d = new Derived;
    Derived* d2 = d->clone();

This is the ideal solution; there is no run-time cost associated with it, and it's completely safe: the compiler will check all our types f or us. If somebody changes Derived::clone() to return a Base*, the compiler will complain; you cannot downcast a Base* to a Derived*. If somebody changes copies and pastes the clone line to somewhere that d is act ually a Base*, the compiler will again complain; d->clone() will return a Base*, which you cannot as sign into a Derived*.

Fortunately, a language feature does exist to do this: covariant return types. If a derived class method returns a more-derived type than its overridden base class method, the derived class return type is said to be covariant.

In code, this is simply expressed by changing the return type of the affected methods:

class Base {
public:
    virtual Base* clone() const
    {
        return new Base(this);
    }
};

class Derived : public Base {
public:
    virtual Derived* clone() const
    {
        return new Derived(this);
    }
};

Of course, the return type does not have to be connected with the class in which the method resides. A perhaps illuminating example:

/* Inheritance hierarchies

         NetServer
             |
             ^
            / \
NetServerTCP   NetServerSCTP


         NetClient
             |
             ^
            / \
NetClientTCP   NetClientSCTP

*/

class NetServer {
public:
    virtual NetClient* acceptConnection() = 0;
};

class NetServerTCP : public NetServer {
public:
    virtual NetClientTCP* acceptConnection();
};

class NetServerSCTP : public NetServer {
public:
    virtual NetClientSCTP* acceptConnection();
};

Covariant return types work with protected and private inheritance (these simply affect the access levels of the revelant functions) and wi th multiple inheritance. Note that gcc-3.4 is the first version of the compiler to claim "non-trivial" support for covariant return types.