(In a future post, I'll write about mixing std::unique_ptr with covariant return types in C++. But it's necessary to introduce covariance first. Thus, here goes.)
Covariance, Part 1: DefinitionsLet us have class
Animal and subclass
Cat. Thus, each
Cat is an
Animal; wherever an
Animal is expected, we can submit a
Cat. But not every
Animal is a
Cat.
Now we have a
Source<Cat> that can provide a
Cat on demand, e.g., a pregnant female cat, or a crusty old cat-hoarding lady.
Wherever
Source<Animal> is expected, we can successfully submit the lady. The lady provides a
Cat which is also an
Animal, thus she fulfils the requirements to be a
Source<Animal>.
We say that
Source<T> is
covariant in its type argument
T. Both of these arrows point into the
same direction:
Cat ------------- is a ------------> Animal
Source<Cat> ----- is a ----> Source<Animal>
Now we have a
Sink<Animal>, e.g., an animal shelter.
When somebody cannot care anymore for their
Cat and asks where the next
Sink<Cat> is, we can point them to the animal shelter. The shelter will accept any
Animal, in particular, it will accept a
Cat.
We say that
Sink<T> is
contravariant in its type argument
T. These arrows point into
opposite directions:
Cat ------------- is a ------------> Animal
Sink<Cat> <------ is a ------- Sink<Animal>
Now consider the types
List<Animal> and
List<Cat> that allow insertion and lookup. Assuming a strong static type system,
List<Cat> guarantees that only a
Cat may get inserted, and also guarantees that anything inside is a
Cat.
Where a
List<Animal> is expected, can we submit a
List<Cat>? No, because the code might then try to insert
Dog into
List<Cat> because that code believes it has a
List<Animal> where insertion of
Dog would be allowed.
Where a
List<Cat> is expected, can we submit a
List<Animal>? No, because the code might then try to look up the first
Cat of the
List<Animal> because the code believes it has a
List<Cat>, but instead might get a
Dog from the
List<Animal>.
We say that
List<T> is
invariant in its type argument
T. There is no subtype relationship between
List<Animal> and
List<Cat>.
Example. A machine that clones an object, i.e., gives back the same object and an equal copy, is
covariant: An animal cloner gives back an animal and a copy, a cat cloner also gives back an animal and a copy. Really, a
T cloner is a
Source<T>.
Example. A
Sink<Sink<T>> is
covariant in
T. The arrow gets inverted twice.
Cat ---------------- is a ---------------> Animal
Sink<Cat> <--------- is a ---------- Sink<Animal>
Sink<Sink<Cat>> ---- is a ---> Sink<Sink<Animal>>Real-world examples of
Sink<Sink<T>> are convoluted and misleading, e.g., burger stores that only cater to zoo keepers who feed only cats? This occurs more naturally in software, e.g., an algorithm might be best described as a higher-order function that takes as argument a function that takes
T.
C++ and D allow
covariant return types when you override a function that returns a pointer. Thus, in the following C++ code,
class Animal {
public:
virtual Animal* clone() { return new Animal(); }
};
class Cat : public Animal {
public:
virtual Cat* clone() override { return new Cat(); }
};... it is legal to override
Animal* clone() to have the stricter return type
Cat* which converts implicitly to
Animal*. Virtual dispatch behaves the same as usual, i.e., calling
animal->clone() gives you an
Animal* that might well come from
Cat::clone.
This is good for two reasons.
- The type system can check more rigorously what you implement in Cat::clone while class Cat gets compiled, even if the method call later will only ever happen via virtual dispatch from an Animal*.
- Nonvirtual callers who definitely have a Cat will be guaranteed to get Cat*, not Animal*, which is useful in a world that deals only with Cat, e.g., during implementation of other Cat methods.
My plan is to write another post on how this fails with
std::unique_ptr<Animal> instead of
Animal*, and discuss workarounds.

-- Simon