Is a Circle a kind of an Ellipse in C++?
01 Dec 2016I stumbled upon this question while reading about inheritance in C++.
The problem is described there in detail, and the author gives good reasons
why a Circle
is not a kind of an Ellipse
.
The main argument is that you may end up with
non-round circles if you inherit Circle
from Ellipse
.
Indeed, if Circle
derives from Ellipse
, you can refer to a circle
through a reference to an ellipse Ellipse&
.
Once you have such a reference,
you can do evil things to your poor little circle.
For example, you may resize it asymmetrically!
That wouldn’t be nice, would it? But why do we come to such result? It feels so
natural that a circle is just a special kind of an ellipse. In the end,
every book on C++ has an example with Circle
inheriting from Shape
and
Smiley
inheriting from Circle
, and everything works like charm
in the textbooks.
To get to the root of all evil, let’s formulate the problem in clear mathematical terms.
Circles and ellipses as sets of points
What is a circle?
I always find it easier to parse such definitions when they are written in the set-builder notation. A circle of radius $a \in \mathbb{R}$ is
Similarly, an ellipse with semi-axes $a \in \mathbb{R}$ and $b \in \mathbb{R}$ is
In C++ terminology, $C_a$ is an object of type Circle
for every fixed $a$.
Analogously, $E_{a,b}$ is an object of type Ellipse
for every fixed pair $\left(a, b\right)$.
Types in C++ as sets of objects
Circles of different radius are different objects.
One might be tempted to think about an object as being a persistent entity,
but that mode of thinking is erroneous.
For example, say, we have a circle c
of radius $1$, and we call
c.size(5)
to set its radius to $5$. Did we just create a new object or did
we modify an existing one? The correct answer is that we have created a new
object. It doesn’t matter that on the implementation level we just
changed a few bits at the same memory location where our old object resided.
What does stay the same, however, is the variable c
that
acts like a box in which we are putting our objects.
The language itself should be your guide.
When you define int i = 1;
, you create a “variable of type int
”, meaning
that i
denotes a box for objects of type int
.
Alright, we now understand that a variable is an imaginary box that can hold objects of a certain type. What is a type then?
Imagine you want to declare a new type called Bool
. By our definition, you
have to provide a set of values that a Bool
can take. That is easy to do,
since those are just 0
and 1
. In addition to that, you need to declare
what one can do with those values; in other words, you have to declare
functions like &&
, ||
, <<
, and so on. Notice that all functions that
can do something on $V$ are part of the type—not only the functions
from $V$ to $V$. That is, void print(Bool)
and Bool::from_int(int)
,
for instance, belong to $F$, despite the fact that the codomain of the former
is the empty set and the domain of the latter is int
.
Thus, a type is a set of objects together with a set of mappings from and to the set of objects.
Circle and Ellipse types
We have defined a circle of radius $a$ in \eqref{circle}. Let’s now form the set of all circles
Analogously for ellipses \eqref{ellipse},
Every circle $C_a$ lives in the family of circles $C$,
and every ellipse $E_{a,b}$—in the family of ellipses $E$. Therefore,
we can identify the type Circle
with the set $C$,
and the type Ellipse
with the set $E$.
Then, to say that an object $c$ is of type $C$ simply means $c \in C$.
Inheritance in C++ as supersetting
A word about inheritance is in order. We are only concerned with public
inheritance here. By definition, a derived type is a strict superset
of the base type it derives from.
Every object of type Derived includes Base as a subobject.
That is, a child can do everything a parent can do, plus something extra.
There are, unfortunately, some quirks in terminology
due to the fact that a derived type
is a superset and not a subset of the base type.
Namely, it is wrong to call a derived type a subtype of the base type,
because it is, in fact, a supertype. Example: let $B$ and $D$ be types, and
let $D$ be derived from $B$; then $B \subset D$. In other words,
if we identify Base
with $B$ and Derived
with $D$,
then Base
is a subtype of Derived
. This is, by the way, why
$D$ is called “derived class” and not “subtype” of $B$.
Maybe an Ellipse is a kind of a Circle?
At this point, it should be apparent that $C$ is a subset of $E$, $C \subset E$.
Indeed, you can find every imaginable circle in the set of ellipses. But by
definition of inheritance, it means precisely that $E$ inherits from $C$!
In other words, Ellips
derives from Circle
(or equivalently,
Circle
is a subtype of Ellipse
).
So, we should inherit Ellipse
from Circle
and not the other way around!
Wait a second. Does this imply that an ellipse is a kind of a circle then?
Relation “is a kind of” is ill-defined
There is no answer to this question, because the relation “is a kind of” is
ill-defined. You can say that a circle is a round ellipse, or you can say
that an ellipse is an oblate circle. Think about int
and double
.
You can say that double
is an int
with higher precision, or you can say
that int
is a less precise double
.
Therefore, the question whether a Circle
is a kind of an Ellipse
does not make sense.
But that does not mean that the whole discussion was futile.
One of the big advantages of class hierarchies is in enabling
substitution of a Base
object in place of a Derived
object.
In our case, a meaningful question would be
“Should Circle
derive from Ellipse
or the other way around?”
And this is the question that we were actually able to answer.
Does Ellipse have radius()
?
We have figured out that, from an abstract point of view, Ellipse
should
derive from Circle
. If you look closely at the argument, though, you will
notice that I cheated a bit. I considered Circle
and Ellipse
as
pure value types, meaning that there are no functions defined on them:
$C = (V, \emptyset)$ and $E = (W, \emptyset)$, where $V$ and $W$ are value sets.
But classes without functions would be totally useless,
so now it’s time to get rid of this simplifying assumption.
Let’s allow Circle
and Ellipse
to have methods. Since Ellipse
inherits
from Circle
, most of them should be virtual. Indeed, formulas for the
arc length and for the surface area are more complicated for the Ellipse
,
and it would be a waste of resources calculating the circumference of a Circle
using the elliptic integral of the second kind,
despite it yielding the correct result.
Ok—you might say—and
what about the methods that Circle
has but Ellipse
doesn’t?
For example, what should Circle::radius()
return for an Ellipse
?
If someone is calling radius()
on an ellipse, the ellipse is apparently
treated as a circle; since Ellipse
has two parameters while Circle
requires only one, you can pick any of the two and use it as radius.
If you additionally make sure that Ellipse::radius(float r)
sets both
$a$ and $b$ to $r$, you can switch between viewing an Ellipse
as an Ellipse
and as a Circle
in a consistent way.
Mind the application during design
Is it worth using inheritance at all in this case? If I have to redefine all
methods of Ellipse
,
why bother inheriting from Circle
in the first place?
And this is a valid point. The answer depends on the application, however.
In some cases, it is better to derive them both from a common ancestor.
In other cases, it is better to define a
suitable Concept that they both satisfy. The final decision should be dictated
by the needs of the application.