热度 20
2013-7-30 20:16
3547 次阅读|
1 个评论
I'm continuing my discussion on 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; colour outline, fill; }; void shape_construct(shape *me, colour o, colour 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 initialise its members. Unfortunately, given the declarations thus far, this straightforward initialisation won't compile: circle_vtbl the_circle_vtbl = { circle_area, // initializes base.area circle_perimeter // initializes base.perimeter }; Let me explain why. 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 initialise 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 about initializing 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 by composition is that it doesn't scale up very well to handle deep class hierarchies. 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 changes depending on how deep the target is in the hierarchy. For example, to compute 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. With inheritance by copying and editing, the notation for a virtual call is the same for every type in a hierarchy. Unfortunately, inheritance by copying and editing has other problems. I'll look at the pros and cons of inheritance by copying and editing in my next column. End notes: 1. Saks, Dan, "Implementing virtual functions in C," Eetindia.co.in, August 14, 2012. http://forum.eetindia.co.in/BLOG_ARTICLE_13544.HTM . 2. Saks, Dan, "Alternative ways to mimic inheritance in C," Eetindia.co.in, April 9, 2013. http://forum.eetindia.co.in/BLOG_ARTICLE_16744.HTM .