For some time 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 initialise derived polymorphic objects.3 My colleague, Miro Samek, posted a comment suggesting a slightly different approach. Last February, 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 realised 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.
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 realised 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.
Endnotes
1. Saks, Dan, "Implementing virtual functions in C++," Eetindia.com, April 2012. http://forum.eetindia.co.in/BLOG_ARTICLE_11885.HTM.
2. Saks, Dan, "Implementing virtual functions in C," Eetindia.com, August 2012. http://forum.eetindia.co.in/BLOG_ARTICLE_13544.HTM.
3. Saks, Dan, "Learn to initialise derived polymorphic objects," Eetindia.com, November 2012. http://forum.eetindia.co.in/BLOG_ARTICLE_14949.HTM.
4. Saks, Dan, "Ways to implement a derived class vtbl in C," Eetindia.com, February 2013. http://forum.eetindia.co.in/BLOG_ARTICLE_16330.HTM.
5. Saks, Dan, "The concept of pure virtual functions," Eetindia.com, December 2012. http://forum.eetindia.co.in/BLOG_ARTICLE_15551.HTM.
6. Hunt, Andrew and Thomas, David, The Pragmatic Programmer. Reading, MA: Addison Wesley Longman, 2000.
文章评论(0条评论)
登录后参与讨论