tag 标签: polymorphic

相关博文
  • 热度 17
    2013-2-22 11:22
    3202 次阅读|
    0 个评论
    For several months now, I've been discussing polymorphic types and virtual functions. I explained how to use virtual functions in C++ as well as how to implement and use virtual functions in C. , More specifically, 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 how to initialise derived polymorphic objects. My colleague, Miro Samek, raised a very interesting question about one of my implementation choices, and offered an alternative approach. While his alternative has merit, I still think my approach is preferable and I'll try to explain why. In any event, it's worth considering both techniques because the discussion raises interesting questions about interface design and maintainability. I'll begin this month by explaining Miro's question in detail. 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. The C++ definition for the shape base class looks in part like: class shape { public: shape(color o, color f); // constructor virtual double area() const; virtual double perimeter() const; private: color outline, fill; }; In C, the comparable declarations look like: // shape.h—a C base class for shapes #ifndef SHAPE_H_INCLUDED #define SHAPE_H_INCLUDED typedef struct shape shape; typedef struct shape_vtbl shape_vtbl; struct shape_vtbl { double (*area)(shape const *s); double (*perimeter)(shape const *s); }; struct shape { shape_vtbl *vptr; color outline, fill; }; void shape_construct(shape *s, color o, color f); double shape_area(shape const *s); double shape_perimeter(shape const *s); #endif In C++, the definition for a circle class derived from shape looks like: class circle: public shape { public: circle(double r, color o, color f); // constructor virtual double area() const; virtual double perimeter() const; ~~~ private: double radius; }; I rendered the derived class in C using the following declarations: // circle.h – a C class for circle derived from shape #ifndef CIRCLE_H_INCLUDED #define CIRCLE_H_INCLUDED #include "shape.h" typedef struct circle circle; struct circle { shape base; // the base class subobject double radius; }; void circle_construct(circle *c, double r, color o, color f); double circle_area(circle const *c); double circle_perimeter(circle const *c); #endif When I showed this header in previous articles, I omitted the declarations for circle_area and circle_perimeter (shown above in red ). I didn't need them in the prior discussion, so I left them out for brevity. However, in general, they should be there. The corresponding C source file looks in part like: // circle.c—circle implementation ~~~ #include "circle.h" ~~~ typedef struct circle_vtbl circle_vtbl; struct circle_vtbl { double (*area)(circle const *); double (*perimeter)(circle const *); }; static circle_vtbl the_circle_vtbl = { circle_area, circle_perimeter }; void circle_construct(circle *c, double r, color o, color f) { shape_construct(c-base, o, f); c-base.vptr = (shape_vtbl *)the_circle_vtbl; c-radius = r; } If you want circle_construct to be an inline function or function-like macro, you must move its definition to the circle.h header, along with the circle_vtbl structure declaration. You also have to remove the keyword static from the definition for the_circle_vtbl, and add an extern declaration for it to the header. As I explained in an earlier column, a derived class inherits all the data members of its base class. Each inherited member must have the same offset within the derived class as it does in the base class. The simplest and most straightforward way to ensure this is to define a base class object as the first member of the derived class, just as I did previously: typedef struct circle circle; struct circle { shape base; // the base class subobject double radius; }; The derived class vtbl inherits all the members of its base class vtbl, and again each inherited member must have the same offset in the derived class vtbl as it does in the base class vtbl. Thus, it seems appropriate to use the same technique for implementing the derived class vtbl. That is, declare a base class vtbl object as the first member of the derived class vtbl, as in: typedef struct circle_vtbl circle_vtbl; struct circle_vtbl { shape_vtbl base; // base class vtbl // virtual functions introduced in circle }; That's what Miro suggested, but that's not what I did. Rather, I defined each member of the derived class vtbl individually to correspond to the members in base class. That is, I defined the base class vtbl structure as: typedef struct shape_vtbl shape_vtbl; struct shape_vtbl { double (*area)(shape const *s); double (*perimeter)(shape const *s); }; and I defined the derived class vtbl structure as: typedef struct circle_vtbl circle_vtbl; struct circle_vtbl { double (*area)(circle const *); double (*perimeter)(circle const *); }; Therein lies Miro's question: Why did I declare the inherited vtbl members individually rather than declare a base class vtbl object as the first member of the derived class vtbl? I'll answer the question next time. In the meantime, you might want to ponder why the corresponding members in the base and derived class vtbl are not exactly the same. References Saks, Dan, "Understanding virtual functions in C++," Eetindia.co.in, April 7, 2012. http://forum.eetindia.co.in/BLOG_ARTICLE_11885.HTM. Saks, Dan, "Implementing virtual functions in C," Eetindia.co.in, August 14, 2012. http://forum.eetindia.co.in/BLOG_ARTICLE_13544.HTM. Saks, Dan, "Learn to initialise derived polymorphic objects," Eetindia.co.in, November 22, 2012. http://forum.eetindia.co.in/BLOG_ARTICLE_14949.HTM. Ibid . Saks, Dan, "Setting storage layout for polymorphic objects," Eetindia.co.in, July 27, 2012. http://forum.eetindia.co.in/BLOG_ARTICLE_13258.HTM.  
  • 热度 21
    2012-12-20 18:43
    3030 次阅读|
    0 个评论
    In the past several months, I have discussed polymorphic types and virtual functions. I showed how to implement virtual functions in C in a way that generates machine code similar to what you get with virtual function in C++.More specifically, 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 month, I showed how to initialise the vptr in base class objects . This month, I'll look at initializing derived class objects. 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. The C++ definition for the shape base class looks in part like: class shape { public: shape(color o, color f); // constructor virtual double area() const; virtual double perimeter() const; private: color outline, fill; }; In C, the comparable declarations look like: // shape.h—a C base class for shapes #ifndef SHAPE_H_INCLUDED #define SHAPE_H_INCLUDED typedef struct shape shape; typedef struct shape_vtbl shape_vtbl; struct shape_vtbl { double (*area)(shape const *s); double (*perimeter)(shape const *s); }; struct shape { shape_vtbl *vptr; color outline, fill; }; void shape_construct(shape *s, color o, color f); double shape_area(shape const *s); double shape_perimeter(shape const *s); #endif As I showed last month, you can define the shape vtbl object in a source file that also defines the member functions of the shape "class": // shape.c—a C base class for shapes #include "shape.h" ~~~ static shape_vtbl the_shape_vtbl = { shape_area, shape_perimeter }; void shape_construct(shape *s, color o, color f) { s-vptr = the_shape_vtbl; s-outline = o; s-fill = f; } In C++, the definition for a circle class derived from shape looks like: class circle: public shape { public: circle(double r, color o, color f); // constructor virtual double area() const; virtual double perimeter() const; ~~~ private: double radius; }; Derivation defines an "is a" or "is a kind of" relationship between the derived and base class. That is, it lets you substitute a derived class object, such as a circle or rectangle, for a base class shape object. For example, given a C++ function such as: void f(shape *p) { ~~~ p-perimeter(); // virtual call to shape's perimeter ~~~ } you can pass it a derived class object, as in: circle c; ~~~ f(c); // pass a circle as a shape and it computes the circle's perimeter correctly. With a little more effort, you can emulate this behaviour in C. In C, you have to explicitly mention the vptr in virtual function calls, as in: void f(shape *p) { ~~~ p-vptr-perimeter(p); virtual call to shape's perimeter ~~~ } You also need an explicit cast to convert a "pointer to derived" into "pointer to base", as in: circle c; ~~~ f((shape *)c); // pass a circle as a shape This substitution works (f will compute the circle's perimeter correctly) only if the vptr has the same offset in the derived class object as it does in a base class object. The easiest way to satisfy this requirement is to implement each derived class type in C as a structure whose first member has the base class type, as in: // circle.h – a C class for circle derived from shape #ifndef CIRCLE_H_INCLUDED #define CIRCLE_H_INCLUDED #include "shape.h" typedef struct circle circle; struct circle { shape base; // the base class subobject double radius; }; void circle_construct(circle *c, double r, color o, color f); #endif The base member of the circle structure above includes all the members inherited from the shape base class, including vptr. The definition for the circle_construct function appears, along with the circle vtbl object, in a separate source file: // circle.c—circle implementation ~~~ #include "circle.h" double circle_area(circle const *c) { return PI * c-radius * c-radius; } double circle_perimeter(circle const *c) { return 2 * PI * c-radius; } typedef struct circle_vtbl circle_vtbl; struct circle_vtbl { double (*area)(circle const *); double (*perimeter)(circle const *); }; static circle_vtbl the_circle_vtbl = { circle_area, circle_perimeter }; void circle_construct(circle *c, double r, color o, color f) { shape_construct(c-base, o, f); c-base.vptr = (shape_vtbl *)the_circle_vtbl; c-radius = r; } The circle_construct function implements behaviour comparable to a C++ constructor. It calls the shape_construct function to initialise the base class part. However, shape_construct sets the vptr to point to shape's vtbl, which is correct for the base class shape, but not for the derived class circle. Thus, circle_construct needs to reassign the vptr to point to circle's vtbl. This assignment requires a cast because circle_vtbl isn't exactly the same type as shape_vtbl. The two structures have the same memory layout, but the corresponding pointers in the different structures point to functions with slightly different types. Thus, the derived class constructor contains two assignments to the vptr. The second assignment completely overwrites the value assigned by the first. Ideally, the compiler will "optimise away" the first assignment. However, the compiler can do this optimisation only if it can see both assignments in the context of the derived class constructor, which it can do only if the base class constructor is an inline function. If you define the shape_construct function as inline, you should move the function definition to the shape.h header file. When you do that, you must also give the_shape_vtbl external linkage by removing the keyword static from its definition, as in: // shape.c—a C base class for shapes ~~~ shape_vtbl the_shape_vtbl = { // used to be static shape_area, shape_perimeter }; ~~~ If your C compiler supports the inline keyword (from C99), then the shape.h header would look in part 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 *s); double (*perimeter)(shape const *s); }; extern shape_vtbl the_shape_vtbl; struct shape { shape_vtbl *vptr; color outline, fill; }; inline void shape_construct(shape *s, color o, color f) { s-vptr = the_shape_vtbl; s-outline = o; s-fill = f; } If your compiler doesn't support the keyword inline, then you can implement the constructor as a macro: #define shape_construct(s, o, f) ( \ (s)-vptr = the_shape_vtbl, \ (s)-outline = (o), \ (s)-fill = (f) \ ) Either way, a compiler with a decent optimiser should eliminate the redundant assignment to the vptr. Once again, you don't have to worry about any of this in C++. With C++, the compiler automatically generates code to initialise the vptr properly.
  • 热度 24
    2012-11-22 20:57
    3035 次阅读|
    1 个评论
    I have previously discussed polymorphic types and virtual functions. I showed how to implement virtual functions in C in a way that generates machine code similar to what you get with virtual function in C++.More specifically, 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 month, I showed how to initialise the vptr in base class objects . This month, I'll look at initializing derived class objects. 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. The C++ definition for the shape base class looks in part like: class shape { public: shape(color o, color f); // constructor virtual double area() const; virtual double perimeter() const; private: color outline, fill; }; In C, the comparable declarations look like: // shape.h—a C base class for shapes #ifndef SHAPE_H_INCLUDED #define SHAPE_H_INCLUDED typedef struct shape shape; typedef struct shape_vtbl shape_vtbl; struct shape_vtbl { double (*area)(shape const *s); double (*perimeter)(shape const *s); }; struct shape { shape_vtbl *vptr; color outline, fill; }; void shape_construct(shape *s, color o, color f); double shape_area(shape const *s); double shape_perimeter(shape const *s); #endif As I showed last month, you can define the shape vtbl object in a source file that also defines the member functions of the shape "class": // shape.c—a C base class for shapes #include "shape.h" ~~~ static shape_vtbl the_shape_vtbl = { shape_area, shape_perimeter }; void shape_construct(shape *s, color o, color f) { s-vptr = the_shape_vtbl; s-outline = o; s-fill = f; } In C++, the definition for a circle class derived from shape looks like: class circle: public shape { public: circle(double r, color o, color f); // constructor virtual double area() const; virtual double perimeter() const; ~~~ private: double radius; }; Derivation defines an "is a" or "is a kind of" relationship between the derived and base class. That is, it lets you substitute a derived class object, such as a circle or rectangle, for a base class shape object. For example, given a C++ function such as: void f(shape *p) { ~~~ p-perimeter(); // virtual call to shape's perimeter ~~~ } you can pass it a derived class object, as in: circle c; ~~~ f(c); // pass a circle as a shape and it computes the circle's perimeter correctly. With a little more effort, you can emulate this behaviour in C. In C, you have to explicitly mention the vptr in virtual function calls, as in: void f(shape *p) { ~~~ p-vptr-perimeter(p); virtual call to shape's perimeter ~~~ } You also need an explicit cast to convert a "pointer to derived" into "pointer to base", as in: circle c; ~~~ f((shape *)c); // pass a circle as a shape This substitution works (f will compute the circle's perimeter correctly) only if the vptr has the same offset in the derived class object as it does in a base class object. The easiest way to satisfy this requirement is to implement each derived class type in C as a structure whose first member has the base class type, as in: // circle.h – a C class for circle derived from shape #ifndef CIRCLE_H_INCLUDED #define CIRCLE_H_INCLUDED #include "shape.h" typedef struct circle circle; struct circle { shape base; // the base class subobject double radius; }; void circle_construct(circle *c, double r, color o, color f); #endif The base member of the circle structure above includes all the members inherited from the shape base class, including vptr. The definition for the circle_construct function appears, along with the circle vtbl object, in a separate source file: // circle.c—circle implementation ~~~ #include "circle.h" double circle_area(circle const *c) { return PI * c-radius * c-radius; } double circle_perimeter(circle const *c) { return 2 * PI * c-radius; } typedef struct circle_vtbl circle_vtbl; struct circle_vtbl { double (*area)(circle const *); double (*perimeter)(circle const *); }; static circle_vtbl the_circle_vtbl = { circle_area, circle_perimeter }; void circle_construct(circle *c, double r, color o, color f) { shape_construct(c-base, o, f); c-base.vptr = (shape_vtbl *)the_circle_vtbl; c-radius = r; } The circle_construct function implements behaviour comparable to a C++ constructor. It calls the shape_construct function to initialise the base class part. However, shape_construct sets the vptr to point to shape's vtbl, which is correct for the base class shape, but not for the derived class circle. Thus, circle_construct needs to reassign the vptr to point to circle's vtbl. This assignment requires a cast because circle_vtbl isn't exactly the same type as shape_vtbl. The two structures have the same memory layout, but the corresponding pointers in the different structures point to functions with slightly different types. Thus, the derived class constructor contains two assignments to the vptr. The second assignment completely overwrites the value assigned by the first. Ideally, the compiler will "optimise away" the first assignment. However, the compiler can do this optimisation only if it can see both assignments in the context of the derived class constructor, which it can do only if the base class constructor is an inline function. If you define the shape_construct function as inline, you should move the function definition to the shape.h header file. When you do that, you must also give the_shape_vtbl external linkage by removing the keyword static from its definition, as in: // shape.c—a C base class for shapes ~~~ shape_vtbl the_shape_vtbl = { // used to be static shape_area, shape_perimeter }; ~~~ If your C compiler supports the inline keyword (from C99), then the shape.h header would look in part 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 *s); double (*perimeter)(shape const *s); }; extern shape_vtbl the_shape_vtbl; struct shape { shape_vtbl *vptr; color outline, fill; }; inline void shape_construct(shape *s, color o, color f) { s-vptr = the_shape_vtbl; s-outline = o; s-fill = f; } If your compiler doesn't support the keyword inline, then you can implement the constructor as a macro: #define shape_construct(s, o, f) ( \ (s)-vptr = the_shape_vtbl, \ (s)-outline = (o), \ (s)-fill = (f) \ ) Either way, a compiler with a decent optimiser should eliminate the redundant assignment to the vptr. Once again, you don't have to worry about any of this in C++. With C++, the compiler automatically generates code to initialise the vptr properly.  
  • 热度 18
    2012-10-24 18:13
    3296 次阅读|
    0 个评论
    On and off for the past year, I've been discussing polymorphic types and virtual functions. A couple of months ago, I showed how to implement virtual functions in C in a way that generates machine code similar to what you get with virtual functions in C++. In particular, I showed that you can 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). Of course, virtual function calls work only if you initialise each vptr properly. That initialisation is my topic this month. As in my 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 definition for the base class shape looks in part like: class shape { public: shape(); // constructor virtual double area() const; virtual double perimeter() const; private: color outline, fill; }; As at least one reader has observed, the virtual functions in the shape class should probably be pure virtual functions and the destructor should be virtual. I haven't covered these topics yet, and they're not crucial to this discussion. You can implement a polymorphic shape type in C using the following declarations: // shape.h—a base class for shapes #ifndef SHAPE_H_INCLUDED #define SHAPE_H_INCLUDED typedef struct shape shape; typedef struct shape_vtbl shape_vtbl; struct shape_vtbl { double (*area)(shape const *s); double (*perimeter)(shape const *s); }; struct shape { shape_vtbl *vptr; color outline, fill; }; void shape_construct(shape *s); double shape_area(shape const *s); double shape_perimeter(shape const *s); #endif      Each shape object has a vptr that must be initialized to point to a vtbl object. Every shape that actually is a shape (rather than a circle , rectangle or triangle ) uses the same virtual functions, so the program needs only one vtbl object for every shape . In C, the vtbl object should be defined in the source file that also defines the member functions of the shape "class": // shape.c—a base class for shapes #include "shape.h" double shape_area(shape const *s) { /* compute and return something */; } double shape_perimeter(shape const *s) { /* compute and return something */; } static shape_vtbl the_shape_vtbl = { shape_area, shape_perimeter }; void shape_construct(shape *s) { s-vptr = the_shape_vtbl; } The definition for the_shape_vtbl has an initializer that specifies a value for each function pointer. The shape_construct function shown above initializes a shape object's vptr to point to the_shape_vtbl . You might want to augment that function to initialise other data members in a shape , as in: void shape_construct(shape *s) { s-vptr = the_shape_vtbl; s-outline = /* an appropriate default */; s-fill = /* an appropriate default */; } or as: void shape_construct(shape *s, color o, color f) { s-vptr = the_shape_vtbl; s-outline = o; s-fill = f; } In C++, you don't declare the vptr member and you don't write any code to initialise it. The compiler does all that automatically. If you choose not to initialise the outline and fill members explicitly, you could define the shape constructor in C++ as just: shape::shape() { } The compiler automatically generates code in this constructor to initialise the shape 's vptr. The constructor also initializes the outline and fill members to their default values (which might be no initialisation at all). Moreover, in C++, you don't even have to write the constructor. If you don't declare a shape constructor at all, the compiler will generate one for you that has the same effect as the one defined just above. In C++, you can define a shape object such as: void f() { shape s; ~~~ and the compiler automatically generates a call to the shape constructor to initialise the vptr. In C, you have to write that call explicitly, as in: void f() { shape s; shape_construct(s); ~~~ If you dynamically allocate a shape in C++ using: void f() { shape *ps = new shape; ~~~ the compiler automatically generates a constructor call to initialise that shape 's vptr. Again, in C you have to write that call explicitly, as in: void f() { shape *ps = malloc(sizeof(shape)); shape_construct(ps); ~~~ As I explained in that prior column, in C++, a call to the virtual area function applied to a shape looks exactly like a non-virtual call, as in: ps-area(); In C, the equivalent call looks like: ps-vptr-area(ps); A properly-initialized vptr makes it happen. I'll have more to say about virtual functions in future columns.  
  • 热度 22
    2012-8-14 13:49
    4156 次阅读|
    0 个评论
    Last month, I explained how C++ compilers typically implement virtual functions by illustrating how using virtual functions affects the storage layout for objects ( Setting storage layout for polymorphic objects ). This month, I'll continue by showing how to implement virtual functions in C in a way that generates machine code very similar to what you get from C++. As before, 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 definition for the base class shape looks like: class shape { public: shape(); // constructor virtual double area() const; virtual double perimeter() const; private: coordinates position; color outline, fill; }; The area and perimeter member functions are virtual. A class, such as shape, with a least one virtual function is a polymorphic type . C++ compilers typically add a hidden pointer to each polymorphic type. That pointer is commonly called a vptr and it points to a table of function pointers called a vtbl . You can implement a polymorphic shape type in C using the following declarations: // shape.h—base class for shapes #ifndef SHAPE_H_INCLUDED #define SHAPE_H_INCLUDED typedef struct shape shape; typedef struct shape_vtbl shape_vtbl; struct shape_vtbl { double (*area)(shape const *s); double (*perimeter)(shape const *s); }; struct shape { shape_vtbl *vptr; coordinates position; color outline, fill; }; #endif A class derived from a polymorphic base class will be polymorphic as well, and it inherits the base class's vptr. The vptr must have the same offset in the base class subobject (the base class portion) of a derived class object as it does in a base class object. In C++, the definition for a circle class derived from shape looks like: class circle: public shape { public: circle(double r); // constructor virtual double area() const; virtual double perimeter() const; private: double radius; }; In C, the declarations for a polymorphic circle type look like: // circle.h – circle derived from shape #ifndef CIRCLE_H_INCLUDED #define CIRCLE_H_INCLUDED #include "shape.h" typedef struct circle circle; struct circle { shape base; // the base class sub-object double radius; }; void circle_construct(circle *c, double r); #endif The base member of the circle structure above includes all the members inherited from the shape base class, including vptr. The circle_construct function initializes a circle, including its vptr. I'll cover the details of initializing the vptr in an upcoming column. As I showed last month, the C++ definition for a rectangle class derived from shape looks a lot like the definition for circle: class rectangle: public shape { public: rectangle(double h, double w); virtual double area() const; virtual double perimeter() const; private: double height, width; }; Similarly, the C declarations for a polymorphic rectangle type look a lot like the declarations for circle: // rectangle.h—rectangle interface #ifndef RECTANGLE_H_INCLUDED #define RECTANGLE_H_INCLUDED #include "shape.h" typedef struct rectangle rectangle; struct rectangle { shape base; // the base class sub-object double height, width; }; void rectangle_construct(rectangle *t, double h, double w); #endif In C++, a call to the virtual area function applied to a shape looks exactly like a non-virtual call, as in: shape *s; ~~~ s-area(); If s points to a circle (the dynamic type of *s is circle), then the call above calls circle::area. If s points to a rectangle, then the call above calls rectangle::area. C doesn't provide explicit support for classes with member functions. In C, you simply use ordinary functions to emulate member functions. For example: circle_area(c); applies the circle_area function to circle c. You get the same runtime performance as a C++ member function call, but without any compile-time checking to enforce access control. In C, virtual function calls look unlike any other kind of function call. For example, a call to the virtual area function applied to a shape looks like: shape *s; ~~~ s-vptr-area(s); In this case, if s points to a circle (the dynamic type of *s is circle), then the call above calls circle_area. If s points to a rectangle, then the call above calls rectangle_area. As I hinted earlier, this works only if you initialise the vptr properly, something I'll cover in an upcoming column.