Problems with inheritance by composition
I'm continuing my discussion of how to implement and use virtual functions in C. The basic approach is to implement each class as a C structure and implement each associated method as a function whose first parameter is a pointer to the structure. For each polymorphic class (a class with at least one virtual function), the corresponding C structure has a member called a vptr (VEE-pointer), which points to a table of function pointers called a vtbl (VEE-table).1
Virtual functions are useful only in inheritance hierarchies–collections of classes where some are derived from others. A few of months ago, I showed two distinct ways to mimic inheritance in C, which I call “inheritance by composition” and “inheritance by copying and editing.”2 Although the mechanics of inheritance by composition might be the simpler of the two, I generally prefer inheritance by copying and editing because it yields a better user interface. This month, I'll explain some problems with inheritance by composition that make this technique undesirable.
As in prior articles, my sample classes represent an assortment of two-dimensional geometric shapes such as circle
, rectangle
, and triangle
, all derived from a common base class called shape
. In C, the shape
“class” implementation requires two structures: one for the shape
data members and one for the corresponding shape
vtbl. The C declarations for the shape class look like:
// shape.h - a C base class for shapes~~~typedef struct shape shape;typedef struct shape_vtbl shape_vtbl;struct shape_vtbl { double (*area)(shape const *me); double (*perimeter)(shape const *me);};struct shape { shape_vtbl *vptr; color outline, fill;};void shape_construct(shape *me, color o, color f);double shape_area(shape const *me);double shape_perimeter(shape const *me);
The function declarations at the end of the header declare all of the shape
member functions, even the virtual ones, as non-virtual functions. Each call to a member function (whether virtual or non-virtual) acts upon a specific “target” object. Each member function has a parameter named me
, which the function uses to access its target.
Using “inheritance by composition,” you declare the circle
structures in the circle.h header as:
typedef struct circle circle;struct circle { shape base; // the base class subobject double radius;};typedef struct circle_vtbl circle_vtbl;struct circle_vtbl { shape_vtbl base; // the base class vtbl // virtual functions introduced in circle};
In the circle.c
source file, you define the_circle_vtbl
(the circle vtbl object) and initialize its members. Unfortunately, given the declarations thus far, this straightforward initialization won't compile:
circle_vtbl the_circle_vtbl = { circle_area, // initializes base.area circle_perimeter // initializes base.perimeter};
Let me explain why.
1. Saks, Dan, “Virtual functions in C,” Embedded.com, August 8, 2012. www.embedded.com/4391967.
2. Saks, Dan, “Alternative idioms for inheritance in C,” Embedded.com, April 1, 2013. www.embedded.com/4411013.
As its name suggests, the circle_area
operates on a circle
object:
double circle_area(circle const *me);
However, using inheritance by composition, the base
member of the circle_vtbl
declares its area
member as:
double (*area)(shape const *me);
That is, it operates on a shape
object.
When you try to initialize the_circle_vtbl.base.area
with (the address of) the circle_area
function, the compiler complains that the pointers types are incompatible, which indeed they are. The compiler also complains aboutinitializing the perimeter
member with circle_perimeter
for the same reason. You have to use casts to get the initializers to compile, as in:
circle_vtbl the_circle_vtbl = { (double (*)(shape const *)) circle_area, (double (*)(shape const *)) circle_perimeter};
By the way, one could argue that the initializer really should have another set of nested braces, as in:
circle_vtbl the_circle_vtbl = { { (double (*)(shape const *))circle_area, (double (*)(shape const *))circle_perimeter} };
However, C lets you get by with only one set of braces.
You can avoid these casts by declaring the circle
functions so that they match the vtbl members exactly, namely as:
double circle_area(shape const *me);double circle_perimeter(shape const *me);
But then you need casts to implement the functions, as in:
double circle_area(shape const *me) { circle const *pc = (circle const *)me; return PI * pc->radius * pc->radius;}
I don't know a way to avoid these casts when using inheritance by composition. If you do, please speak up.
These casts are a nuisance, but the real problem with inheritance bycomposition is that it doesn't scale up very well to handle deep classhierarchies. For example, suppose the shape hierarchy looks like:
In this hierarchy, circle
is derived directly from shape
, but rectangle
and triangle
are derived from polygon
, which is derived from shape
.
In C++, the notation for a virtual function call is the same for a pointer to an object anywhere in the hierarchy. For example, given:
double a;shape *ps;polygon *pp;
then you can compute the area for either the shape
or the polygon
using a call of the same form, namely:
a = ps->area();
for the shape
and:
a = pp->area();
for the polygon
.Using inheritance by composition in C, the form of the call changesdepending on how deep the target is in the hierarchy. For example, tocompute the area of a shape
you write:
a = ps->vptr->area(ps);
but to compute the area of a polygon
, you write:
a = pp->base.vptr->area((shape *)pp);
It greats worse the deeper the target type is in the hierarchy.
Inheritance by copying and editing doesn't have this problem. Withinheritance by copying and editing, the notation for a virtual call isthe same for every type in a hierarchy. Unfortunately, inheritance bycopying and editing has other problems.
I'll look at the pros and cons of inheritance by copying and editing in my next column.
Dan Saks is president of Saks & Associates, a C/C++ training and consulting company. For more information about Dan Saks, visit his website at www.dansaks.com. Dan also welcomes your feedback: e-mail him at . .
There are not a lot of scenarios where I would want to call a base class method on a child class object. Constructors are one and could be dealt with by have each child call the base class constructor up the chain.
A more common scenario is having a colle
“Constructors are one and could be dealt with by have each child call the base class constructor up the chain.”
By having each child class constructor call the parent constructor so that if you are many levels down in the hierarchy all you have to do is c
“By the way, one could argue that the initializer really should have another set of nested braces,…”
Why?
A couple of recent reports by Allianz point to the difficulty of retirement in today's poor economy. Most of those approaching retirement are not only unprepared, but often they have no realistic idea of how much cash they need to put away to retire. Conse
I am reading these series of articles rather late – however coudn't the linux kernel container_of macro simplify code in getting the outer structure ? Also I wouldn't prefer that client code do any traversal – ideally client code sees only opaque types and
“http://goo.gl/mxSH7Qnhttps://goo.gl/xKw64snhttps://goo.gl/rzvJTgnhttp://goo.gl/rI3Yynbttp://goo.gl/nPcb9qnhttp://goo.gl/YKcFjwnhttp://goo.gl/AaXsd2nhttp://goo.gl/MX3RScnhttps://goo.gl/aKGaClnhttp://goo.gl/IsfzqMnhtt
“Hi azwar. I open your post but they are not in english. What is meant by these posts. Thanks”