Is a Circle a kind of an Ellipse in C++?

I 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?

A circle is the set of all points in a plane that are at a given distance from a given point.

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?

A type $T$ (also referred to as class in C++) is a set of values $V$ together with a set of functions $F$ from $V$ and to $V$.

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.

An object $x$ of type $T = (V, F)$ is an element of $V$.

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.