Off and on for about two years, I've been writing about techniques for representing and manipulating memory-mapped devices in C and C++. My more recent columns have been more about C++ than C, focusing on language features such as constructors and new-expressions, which C++ doesn't share with C.1, 2
Some readers have suggested that the C code I presented is preferable to the C++ code because the C structure implementations for devices are generally simpler than their corresponding C++ class implementations. Last month, I argued that the C++ implementations are actually better because they're easier to use correctly and harder to use incorrectly.
Other readers have claimed that my C++ implementations are flawed because they're too simple—they don't use inheritance and preclude the use of virtual functions.
Classes with virtual functions can be very useful, but they aren't the solution to every problem. Classes that represent memory-mapped devices, such as the ones I've presented, work with real hardware specifically because they don't use virtual functions. The new C++ Standard acknowledges the usefulness of such classes by defining categories such as standard layout classes, which avoid features such as virtual functions.
In the coming months, I'll explain what virtual functions are. I'll show why they can be useful in some applications, but undesirable in classes that represent memory-mapped device registers. I'll also show you how C++ typically implements virtual functions by showing how you can emulate them in C.
I'll begin this month by looking at the sort of problem that virtual functions are good at solving. I'll show a typical C solution using a construct called a discriminated union, and examine its limitations.
An illustrative problem
Suppose you have an application that employs two-dimensional geometric shapes, such as circles, rectangles, and triangles. At a minimum, each shape object contains some linear or angular distances sufficient to characterize the physical extent of the shape. For example, a circle has a radius, a rectangle has a height and a width, and a triangle has two sides and an angle. The shapes may have common attributes as well, such a position (planar coordinates), or outline and fill colors.
A fairly traditional C implementation for a shape is a structure with a nested union, such as:
typedef struct shape shape;
struct shape {
coordinates position;
color outline, fill;
shape_kind kind;
union {
circle_part circle;
rectangle_part rectangle;
triangle_part triangle;
} u;
};
The union member, u, is large enough to hold the largest of its members, but it can only store the value of one member at a time.
In this example, each union member has a different structure type. For a circle, you need to store only the radius, as in:
typedef struct circle_part circle_part;
struct circle_part {
double radius;
};
For a rectangle, you need the height and width:
typedef struct rectangle_part rectangle_part;
struct rectangle_part {
double height, width;
};
For a triangle, you need two sides and an adjacent angle:
typedef struct triangle_part triangle_part;
struct triangle_part {
double side1, side2, angle;
};
When the union members have such simple types, you might find it easier to dispense with the named structure types and simply define unnamed structures inside the union, as in:
union {
struct {
double radius;
} circle;
struct {
double height, width;
} rectangle;
struct {
double side1, side2, angle;
} triangle;
} u;
The value of the shape's kind member indicates which union member is currently in use. The shape_kind type enumerates the possible values:
enum shape_kind {
sk_circle, sk_rectangle, sk_triangle
};
typedef enum shape_kind shape_kind;
For example, setting a shape's kind member to sk_rectangle indicates that it's now OK to access (write to or read from) the union's rectangle member and its height and width members.
[To be continued at Uses of discriminated unions (Part 1)]
文章评论(0条评论)
登录后参与讨论