Alternative idioms for inheritance in C - Embedded.com

Alternative idioms for inheritance in C

C doesn't really support inheritance, but it offers alternative ways to mimic inheritance.

For more than a year now, I've been explaining how to implement and use virtual functions in C.1,2 I showed how to emulate a polymorphic C++ class (a class with at least one virtual function) as a C structure that has an additional member commonly called a vptr (VEE-pointer). The vptr points to a table of function pointers called a vtbl (VEE-table).

Last November, I showed a way to initialize derived polymorphic objects.3 My colleague, Miro Samek, posted a comment suggesting a slightly different approach. Last month, I explained the difference between the two approaches in detail.4

I had originally planned to explain why I prefer my approach to Miro's. After some very helpful exchanges with Miro, I realized that my implementation doesn't have the interface that I'd like it to have. So this month I'm going back up a bit and present yet another variation on how to implement inheritance and virtual functions in C.

Once again, 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 . I recently suggested that the shape class should be an abstract class—one for which you can't, or at least shouldn't, create objects.5 To simplify this discussion, I'll assume the shape class is not abstract.

In C++, the shape implementation requires just a single class. In C, it 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);

Each shape_vtbl member corresponds to a virtual function in the shape class. In this case, the shape class has two virtual functions: area and perimeter . It also has one non-virtual function: shape_construct , a constructor.

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 virtual function needs a non-virtual implementation so that the corresponding function pointer in the shape_vtbl has something to which it can point.

In C++, every (non-static) member function in a class such as shape has an implicitly declared parameter named this whose type is “pointer to shape ” or “pointer to const shape “. A call such as ps->area() passes ps as the value of the area function's this parameter.

C doesn't declare this parameters implicitly. In C, every shape “member” function needs an explicitly-declared parameter of type “pointer to shape “. (In some cases, it's “pointer to const shape “, but I won't belabor that anymore.) In the shape.h header, I declared that pointer as the first parameter of each member function. In my previous articles, I named those parameters s (for shape ). I could have called them this , but then my code would not compile as C++. In his examples, Miro Samek called the pointers me . For this article, I've adopted his convention. Otherwise, this shape class is pretty much as I presented in previous articles.

In C, each derived class, such as circle , requires two more structures: one for the circle data members and one for the corresponding circle_vtbl . The circle structure “inherits” the data members of the shape structure. That is, the initial portion of the circle structure should have all the data members in the same order as they appear in the shape structure. The simplest way to ensure this is to use “inheritance by composition”—to define a base class object as the first member of the derived class, as in:

typedef struct circle circle;struct circle {    shape base;      // the base class subobject    double radius;};

That is what I did in previous articles.


Footnotes:

The derived class vtbl (that is, the vtbl of the derived class) inherits all the members of its base class vtbl, and each inherited member must have the same offset in the derived class vtbl as it does in the base class vtbl. Miro suggested this is another opportunity to use inheritance by composition, as in:

typedef struct circle_vtbl circle_vtbl;struct circle_vtbl {    shape_vtbl base;     // base class vtbl    // virtual functions introduced in circle};

I didn't use inheritance by composition because it gives the me pointers in the derived class vtbl the wrong type. Rather, I defined each member of the derived class vtbl individually to correspond to the members in base class, as in:

typedef struct circle_vtbl circle_vtbl;struct circle_vtbl {    double (*area)(circle const *me);    double (*perimeter)(circle const *me);};

In general, the me pointer for each member of a class C should have type “pointer to C “. For example, the me pointer in each shape class member should have type “pointer to shape “, and the me pointer in each circle class member should have type “pointer to circle “, as in circle_vtbl above. C++ and other statically-typed object-oriented languages follow this rule for declaring this pointers or their equivalent.

Declaring each me pointer to match its class type requires more effort than using inheritance by composition. You have to use what I call “inheritance by copying and editing”. That is, you have to copy all the members from the base class vtbl to the derived class vtbl, and then change the me pointers from “pointer to base” into “pointer to derived”.

In the course of writing this article, I realized that I had defined the derived class structures circle , rectangle , and triangle using inheritance by composition. Consequently, the vptr member in each derived class had the wrong type. I should have used inheritance by copying and editing to give each “inherited” vptr its correct type.

Rather than define the circle structure as:

typedef struct circle_vtbl circle_vtbl;struct circle_vtbl {    shape_vtbl base;     // base class vtbl    // virtual functions introduced in circle};

I now recommend copying the shape data members to circle , as in:

typedef struct circle circle;struct circle {    circle_vtbl *vptr;       // copied and edited from shape    color outline, fill;     // copied from shape    double radius;};

The vptr member copied from shape is declared as:

    shape _vtbl *vptr

which isn't quite the right type for the derived type. I changed the declaration in circle to:

    circle _vtbl *vptr;

which is exactly the right type.

Such copying and editing arguably violates the DRY Principle (Don't Repeat Yourself). Should the base class change, keeping the derived classes in sync with the base class could be a maintenance headache. Nonetheless, I prefer using inheritance by copying and editing because it leads to type hierarchies with interfaces that look and act more like what C++ offers. Once you get past the initial hierarchy setup, such hierarchies are simpler and safer to use than what you get using inheritance by composition. I'll show you why in an upcoming column. I'll also show how you can use some simple macros that eliminate most of the code duplication.

Thanks to Miro Samek, Steve Dewhurst, Ben Saks, and Joel Saks for their help with this article.

Dan Saks is president of Saks & Associates, a C/C++ training and consulting company. E-mail him at .


Endnotes:

1. Saks, Dan, “Virtual Functions in C++,” Embedded.com, April 4, 2012. www.embedded.com/4370404.

2. Saks, Dan, “Virtual functions in C,” Embedded.com, August 8, 2012. www.embedded.com/4391967.
3. Saks, Dan, “Initializing derived polymorphic objects,” Embedded.com, November 15, 2012. www.embedded.com/4401463.
4. Saks, Dan, “Implementing a derived class vtbl in C,” Embedded.com, February 13, 2013. www.embedded.com/4406901.
5. Saks, Dan, “Pure virtual functions,” Embedded.com , December 12, 2012. www.embedded.com/4403313.
6. Hunt, Andrew and Thomas, David, The Pragmatic Programmer . Reading, MA: Addison Wesley Longman, 2000.

8 thoughts on “Alternative idioms for inheritance in C

  1. Hello,

    that's a very nice approach. What i'm doing, is using a mixture of anonymous unions and structs (of course you need a C11 compiler for this). Something like:

    typedef struct point{
    int x,y;
    }point;

    typedef struct{
    union{
    struct po

    Log in to Reply
  2. GObject does this kind of “C++ in C” too.

    I used a similar approach, but less comprehensive, on a project once. I used it to implement the notion of “streams”. That way I was able to abstract away the source of data for a module. For instance, I had da

    Log in to Reply
  3. I somewhat dislike “inheretance by composition” that places the “base object” as the first part of the “derived object” because:

    a) It makes some assumptions about layout.
    b) It can't support multiple inheretance (not that MI is a good idea…)

    I tend t

    Log in to Reply
  4. The origin of this C11 feature is Ken Thompson’s C compiler for Plan 9—but C11 left out this part of the concept:

    typedef struct Lock Lock;
    void lock(Lock *);

    struct Foo {
    int stuff;
    Lock;
    } *f;

    lock(f); // translates to lock(&(f->Lock))

    Not sure wh

    Log in to Reply
  5. This sounds very dangerous to me.

    C only has very rudimentary type checking and this does away with some of it.

    Automatic casting and such is Very Bad, IMHO. It is far more preferable to do something explicit, easily achieved with C's macro system.

    eg.

    Log in to Reply
  6. If embedded.com really wants to engage the programmer community, then at least get a comment handling system that handles code snippets properly!

    Aaaargh!

    Log in to Reply
  7. Dan,

    Could you make the shape_area() and shape_perimeter() static functions in shape.c so the user has one way to access the functionality?

    Log in to Reply

Leave a Reply

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