Problems with inheritance by composition -

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,”, August 8, 2012.

2. Saks, Dan, “Alternative idioms for inheritance in C,”, April 1, 2013.

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 Dan also welcomes your feedback: e-mail him at . .

7 thoughts on “Problems with inheritance by composition

  1. 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

    Log in to Reply
  2. “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

    Log in to Reply
  3. 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

    Log in to Reply
  4. 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

    Log in to Reply
  5. “

    Log in to Reply

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.