Programming: Principles and Practice Using C++ (2014)

Part II: Input and Output

14. Graphics Class Design

“Functional, durable, beautiful.”

—Vitruvius

The purpose of the graphics chapters is dual: we want to provide useful tools for displaying information, but we also use the family of graphical interface classes to illustrate general design and implementation techniques. In particular, this chapter presents some ideas of interface design and the notion of inheritance. Along the way, we have to take a slight detour to examine the language features that most directly support object-oriented programming: class derivation, virtual functions, and access control. We don’t believe that design can be discussed in isolation from use and implementation, so our discussion of design is rather concrete. Maybe you’d better think of this chapter as “Graphics Class Design and Implementation.”


14.1 Design principles

14.1.1 Types

14.1.2 Operations

14.1.3 Naming

14.1.4 Mutability

14.2 Shape

14.2.1 An abstract class

14.2.2 Access control

14.2.3 Drawing shapes

14.2.4 Copying and mutability

14.3 Base and derived classes

14.3.1 Object layout

14.3.2 Deriving classes and defining virtual functions

14.3.3 Overriding

14.3.4 Access

14.3.5 Pure virtual functions

14.4 Benefits of object-oriented programming


14.1 Design principles

What are the design principles for our graphics interface classes? First: What kind of question is that? What are “design principles” and why do we need to look at those instead of getting on with the serious business of producing neat pictures?

14.1.1 Types

Image

Graphics is an example of an application domain. So, what we are looking at here is an example of how to present a set of fundamental application concepts and facilities to programmers (like us). If the concepts are presented confusingly, inconsistently, incompletely, or in other ways poorly represented in our code, the difficulty of producing graphical output is increased. We want our graphics classes to minimize the effort of a programmer trying to learn and to use them.

Image

Our ideal of program design is to represent the concepts of the application domain directly in code. That way, if you understand the application domain, you understand the code and vice versa. For example:

• Window — a window as presented by the operating system

• Line — a line as you see it on the screen

• Point — a coordinate point

• Color — as you see it on the screen

• Shape — what’s common for all shapes in our graphics/GUI view of the world

The last example, Shape, is different from the rest in that it is a generalization, a purely abstract notion. We never see just a shape on the screen; we see a particular shape, such as a line or a hexagon. You’ll find that reflected in the definition of our types: try to make a Shape variable and the compiler will stop you.

The set of our graphics interface classes is a library; the classes are meant to be used together and in combination. They are meant to be used as examples to follow when you define classes to represent other graphical shapes and as building blocks for such classes. We are not just defining a set of unrelated classes, so we can’t make design decisions for each class in isolation. Together, our classes present a view of how to do graphics. We must ensure that this view is reasonably elegant and coherent. Given the size of our library and the enormity of the domain of graphical applications, we cannot hope for completeness. Instead, we aim for simplicity and extensibility.

In fact, no class library directly models all aspects of its application domain. That’s not only impossible; it is also pointless. Consider writing a library for displaying geographical information. Do you want to show vegetation? National, state, and other political boundaries? Road systems? Railroads? Rivers? Highlight social and economic data? Seasonal variations in temperature and humidity? Wind patterns in the atmosphere above? Airline routes? Mark the locations of schools? The locations of fast-food “restaurants”? Local beauty spots? “All of that!” may be a good answer for a comprehensive geographical application, but it is not an answer for a single display. It may be an answer for a library supporting such geographical applications, but it is unlikely that such a library could also cover other graphical applications such as freehand drawing, editing photographic images, scientific visualization, and aircraft control displays.

Image

So, as ever, we have to decide what’s important to us. In this case, we have to decide which kind of graphics/GUI we want to do well. Trying to do everything is a recipe for failure. A good library directly and cleanly models its application domain from a particular perspective, emphasizes some aspects of the application, and deemphasizes others.

The classes we provide here are designed for simple graphics and simple graphical user interfaces. They are primarily aimed at users who need to present data and graphical output from numeric/scientific/engineering applications. You can build your own classes “on top of” ours. If that is not enough, we expose sufficient FLTK details in our implementation for you to get an idea of how to use that (or a similar “full-blown” graphics/GUI library) directly, should you so desire. However, if you decide to go that route, wait until you have absorbed Chapters 17 and 18. Those chapters contain information about pointers and memory management that you need for successful direct use of most graphics/GUI libraries.

Image

One key decision is to provide a lot of “little” classes with few operations. For example, we provide Open_polyline, Closed_polyline, Polygon, Rectangle, Marked_polyline, Marks, and Mark where we could have provided a single class (possibly called “polyline”) with a lot of arguments and operations that allowed us to specify which kind of polyline an object was and possibly even mutate a polyline from one kind to another. The extreme of this kind of thinking would be to provide every kind of shape as part of a single class Shape. We think that using many small classes most closely and most usefully models our domain of graphics. A single class providing “everything” would leave the user messing with data and options without a framework to help understanding, debugging, and performance.

14.1.2 Operations

Image

We provide a minimum of operations as part of each class. Our ideal is the minimal interface that allows us to do what we want. Where we want greater convenience, we can always provide it in the form of added nonmember functions or yet another class.

Image

We want the interfaces of our classes to show a common style. For example, all functions performing similar operations in different classes have the same name, take arguments of the same types, and where possible require those arguments in the same order. Consider the constructors: if a shape requires a location, it takes a Point as its first argument:

Line ln {Point{100,200},Point{300,400}};
Mark m {Point{100,200},'x'};          // display a single point as an 'x'
Circle c {Point{200,200},250};

All functions that deal with points use class Point to represent them. That would seem obvious, but many libraries exhibit a mixture of styles. For example, imagine a function for drawing a line. We could use one of two styles:

void draw_line(Point p1, Point p2);                        // from p1 to p2 (our style)
void draw_line(int x1, int y1, int x2, int y2);           // from (x1,y1) to (x2,y2)

We could even allow both, but for consistency, improved type checking, and improved readability we use the first style exclusively. Using Point consistently also saves us from confusion between coordinate pairs and the other common pair of integers: width and height. For example, consider:

draw_rectangle(Point{100,200}, 300, 400);            // our style
draw_rectangle(100,200,300,400);                           // alternative

The first call draws a rectangle with a point, width, and height. That’s reasonably easy to guess, but how about the second call? Is that a rectangle defined by points (100,200) and (300,400)? A rectangle defined by a point (100,200), a width 300, and a height 400? Something completely different (though plausible to someone)? Using the Point type consistently avoids such confusion.

Incidentally, if a function requires a width and a height, they are always presented in that order (just as we always give an x coordinate before a y coordinate). Getting such little details consistent makes a surprisingly large difference to the ease of use and the avoidance of run-time errors.

Image

Logically identical operations have the same name. For example, every function that adds points, lines, etc. to any kind of shape is called add(), and any function that draws lines is called draw_lines(). Such uniformity helps us remember (by offering fewer details to remember) and helps us when we design new classes (“just do the usual”). Sometimes, it even allows us to write code that works for many different types, because the operations on those types have an identical pattern. Such code is called generic; see Chapters 1921.

14.1.3 Naming

Image

Logically different operations have different names. Again, that would seem obvious, but consider: why do we “attach” a Shape to a Window, but “add” a Line to a Shape? In both cases, we “put something into something,” so shouldn’t that similarity be reflected by a common name? No. The similarity hides a fundamental difference. Consider:

Open_polyline opl;
opl.add(Point{100,100});
opl.add(Point{150,200});
opl.add(Point{250,250});

Here, we copy three points into opl. The shape opl does not care about “our” points after a call to add(); it keeps its own copies. In fact, we rarely keep copies of the points — we leave that to the shape. On the other hand, consider:

win.attach(opl);

Here, we create a connection between the window win and our shape opl; win does not make a copy of opl — it keeps a reference to opl. So, it is our responsibility to keep opl valid as long as win uses it. That is, we must not exit opl’s scope while win is using opl. We can update opl and the next time win comes to draw opl our changes will appear on the screen. We can illustrate the difference between attach() and add() graphically:

Image

Basically, add() uses pass-by-value (copies) and attach() uses pass-by-reference (shares a single object). We could have chosen to copy graphical objects into Windows. However, that would have given a different programming model, which we would have indicated by using add() rather thanattach(). As it is, we just “attach” a graphics object to a Window. That has important implications. For example, we can’t create an object, attach it, allow the object to be destroyed, and expect the resulting program to work:

void f(Simple_window& w)
{
          Rectangle r {Point{100,200},50,30};
          w.attach(r);
}        // oops, the lifetime of r ends here

int main()
{
          Simple_window win {Point{100,100},600,400,"My window"};
          // . . .
          f(win);          // asking for trouble
          // . . .
          win.wait_for_button();
}

Image

By the time we have exited from f() and reached wait_for_button(), there is no r for the win to refer to and display. In Chapter 17, we’ll show how to create objects within a function and have them survive after the return from the function. Until then, we must avoid attaching objects that don’t survive until the call of wait_for_button(). We have Vector_ref (§13.10, §E.4) to help with that.

Note that had we declared f() to take its Window as a const reference argument (as recommended in §8.5.6), the compiler would have prevented our mistake: we can’t attach(r) to a const Window because attach() needs to make a change to the Window to record the Window’s interest in r.

14.1.4 Mutability

Image

When we design a class, “Who can modify the data (representation)?” and “How?” are key questions that we must answer. We try to ensure that modification to the state of an object is done only by its own class. The public/private distinction is key to this, but we’ll show examples where a more flexible/subtle mechanism (protected) is employed. This implies that we can’t just give a class a data member, say a string called label; we must also consider if it should be possible to modify it after construction, and if so, how. We must also decide if code other than our class’s member functions needs to read the value of label, and if so, how. For example:

struct Circle {
          // . . .
private:
          int r;    // radius
};

Circle c {Point{100,200},50};
c.r = –9;            // OK? No — compile-time error: Circle::r is private

Image

As you might have noticed in Chapter 13, we decided to prevent direct access to most data members. Not exposing the data directly gives us the opportunity to check against “silly” values, such as a Circle with a negative radius. For simplicity of implementation, we take only limited advantage of this opportunity, so do be careful with your values. The decision not to consistently and completely check reflects a desire to keep the code short for presentation and the knowledge that if a user (you, us) supplies “silly” values, the result is simply a messed-up image on the screen and not corruption of precious data.

We treat the screen (seen as a set of Windows) purely as an output device. We can display new objects and remove old ones, but we never ask “the system” for information that we don’t (or couldn’t) know ourselves from the data structures we have built up representing our images.

14.2 Shape

Class Shape represents the general notion of something that can appear in a Window on a screen:

• It is the notion that ties our graphical objects to our Window abstraction, which in turn provides the connection to the operating system and the physical screen.

• It is the class that deals with color and the style used to draw lines. To do that it holds a Line_style, a Color for lines, and a Color for fill.

• It can hold a sequence of Points and has a basic notion of how to draw them.

Experienced designers will recognize that a class doing three things probably has problems with generality. However, here, we need something far simpler than the most general solution.

We’ll first present the complete class and then discuss its details:

class Shape {             // deals with color and style and holds sequence of lines
public:
          void draw() const;                                  // deal with color and draw lines
          virtual void move(int dx, int dy);         // move the shape +=dx and +=dy

          void set_color(Color col);
          Color color() const;

          void set_style(Line_style sty);
          Line_style style() const;

          void set_fill_color(Color col);
          Color fill_color() const;

          Point point(int i) const;                          // read-only access to points
          int number_of_points() const;

          Shape(const Shape&) = delete;          // prevent copying
          Shape& operator=(const Shape&) = delete;

          virtual ~Shape() { }
protected:
          Shape() { }
          Shape(initializer_list<Point> lst);        // add() the Points to this Shape

          virtual void draw_lines() const;           // draw the appropriate lines
          void add(Point p);                                  // add p to points
          void set_point(int i, Point p);               // points[i]=p;
private:
          vector<Point> points;                            // not used by all shapes
          Color lcolor {fl_color()};         // color for lines and characters (with default)
          Line_style ls {0};
          Color fcolor {Color::invisible};           // fill color
};

This is a relatively complex class designed to support a wide variety of graphics classes and to represent the general concept of a shape on the screen. However, it still has only four data members and 15 functions. Furthermore, those functions are all close to trivial so that we can concentrate on design issues. For the rest of this section we will go through the members one by one and explain their role in the design.

14.2.1 An abstract class

Consider first Shape’s constructors:

protected:
          Shape() { }
          Shape(initializer_list<Point> lst);        // add() the Points to this Shape

The constructors are protected. That means that they can only be used directly from classes derived from Shape (using the :Shape notation). In other words, Shape can only be used as a base for classes, such as Line and Open_polyline. The purpose of that protected: is to ensure that we don’t make Shape objects directly. For example:

Shape ss;            // error: cannot construct Shape

Image

Shape is designed to be a base class only. In this case, nothing particularly nasty would happen if we allowed people to create Shape objects directly, but by limiting use, we keep open the possibility of modifications to Shape that would render it unsuitable for direct use. Also, by prohibiting the direct creation of Shape objects, we directly model the idea that we cannot have/see a general shape, only particular shapes, such as Circle and Closed_polyline. Think about it! What does a shape look like? The only reasonable response is the counter question “What shape?” The notion of a shape that we represent by Shape is an abstract concept. That’s an important and frequently useful design notion, so we don’t want to compromise it in our program. Allowing users to directly create Shape objects would do violence to our ideal of classes as direct representations of concepts.

The default constructor sets the members to their default values. Here again, the underlying library used for implementation, FLTK, “shines through.” However, FLTK’s notions of color and style are not mentioned directly by the uses. They are only part of the implementation of our Shape,Color, and Line_style classes. The vector<Points> defaults to an empty vector.

The initializer-list constructor also uses the default initializers, and then add()s the elements of its argument list to the Shape:

Shape::Shape(initializer_list<Point> lst)
{
          for (Point p : list) add(p);
}

Image

A class is abstract if it can be used only as a base class. The other — more common — way of achieving that is called a pure virtual function; see §14.3.5. A class that can be used to create objects — that is, the opposite of an abstract class — is called a concrete class. Note that abstractand concrete are simply technical words for an everyday distinction. We might go to the store to buy a camera. However, we can’t just ask for a camera and take it home. What brand of camera? Which particular model camera? The word camera is a generalization; it refers to an abstract notion. An Olympus E-M5 refers to a specific kind of camera, which we (in exchange for a large amount of cash) might acquire a particular instance of: a particular camera with a unique serial number. So, “camera” is much like an abstract (base) class; “Olympus E-M5” is much like a concrete (derived) class, and the actual camera in my hand (if I bought it) would be much like an object.

The declaration

virtual ~Shape() { }

defines a virtual destructor. We won’t use that for now, so we leave the explanation to §17.5.2, where we show a use.

14.2.2 Access control

Class Shape declares all data members private:

private:
          vector<Point> points;
          Color lcolor {fl_color()};        // color for lines and characters (with default)
          Line_style ls {0};
          Color fcolor {Color::invisible};           // fill color

The initializers for the data members don’t depend on constructor arguments, so I specified them in the data member declarations. As ever, the default value for a vector is “empty” so I didn’t have to be explicit about that. The constructor will apply those default values.

Image

Since the data members of Shape are declared private, we need to provide access functions. There are several possible styles for doing this. We chose one that we consider simple, convenient, and readable. If we have a member representing a property X, we provide a pair of functions X()and set_X() for reading and writing, respectively. For example:

void Shape::set_color(Color col)
{
          lcolor = col;

}

Color Shape::color() const
{
          return lcolor;
}

The main inconvenience of this style is that you can’t give the member variable the same name as its readout function. As ever, we chose the most convenient names for the functions because they are part of the public interface. It matters far less what we call our private variables. Note the way we use const to indicate that the readout functions do not modify their Shape (§9.7.4).

Shape keeps a vector of Points, called points, that a Shape maintains in support of its derived classes. We provide the function add() for adding Points to points:

void Shape::add(Point p)         // protected
{
          points.push_back(p);
}

Naturally, points starts out empty. We decided to provide Shape with a complete functional interface rather than giving users — even member functions of classes derived from Shape — direct access to data members. To some, providing a functional interface is a no-brainer, because they feel that making any data member of a class public is bad design. To others, our design seems overly restrictive because we don’t allow direct write access to all members of derived classes.

A shape derived from Shape, such as Circle and Polygon, knows what its points mean. The base class Shape does not “understand” the points; it only stores them. Therefore, the derived classes need control over how points are added. For example:

• Circle and Rectangle do not allow a user to add points; that just wouldn’t make sense. What would be a rectangle with an extra point? (§12.7.6)

• Lines allows only pairs of points to be added (and not an individual point; §13.3).

• Open_polyline and Marks allow any number of points to be added.

• Polygon allows a point to be added only by an add() that checks for intersections (§13.8).

Image

We made add() protected (that is, accessible from a derived class only) to ensure that derived classes take control over how points are added. Had add() been public (everybody can add points) or private (only Shape can add points), this close match of functionality to our idea of shapes would not have been possible.

Similarly, we made set_point() protected. In general, only a derived class can know what a point means and whether it can be changed without violating an invariant. For example, if we have a Regular_hexagon class defined as a set of six points, changing just a single point would make the resulting figure “not a regular hexagon.” On the other hand, if we changed one of the points of a rectangle, the result would still be a rectangle. In fact, we didn’t find a need for set_point() in our example classes and code, so set_point() is provided just to ensure that the rule that we can read and set every attribute of a Shape holds. For example, if we wanted a Mutable_rectangle, we could derive it from Rectangle and provide operations to change the points.

We made the vector of Points, points, private to protect it against undesired modification. To make it useful, we also need to provide access to it:

void Shape::set_point(int i, Point p)       // not used; not necessary so far
{
          points[i] = p;
}

Point Shape::point(int i) const
{
          return points[i];
}

int Shape::number_of_points() const
{
          return points.size();
}

In derived class member functions, these functions are used like this:

void Lines::draw_lines() const
          // draw lines connecting pairs of points
{
          for (int i=1; i<number_of_points(); i+=2)
                    fl_line(point(i–1).x,point(i–1).y,point(i).x,point(i).y);
}

Image

You might worry about all those trivial access functions. Are they not inefficient? Do they slow down the program? Do they increase the size of the generated code? No, they will all be compiled away (“inlined”) by the compiler. Calling number_of_points() will take up exactly as many bytes of memory and execute exactly as many instructions as calling points.size() directly.

These access control considerations and decisions are important. We could have provided this close-to-minimal version of Shape:

struct Shape {            // close-to-minimal definition — too simple — not used
          Shape();
          Shape(initializer_list<Point>);
          void draw() const;                            // deal with color and call draw_lines
          virtual void draw_lines() const;     // draw the appropriate lines
          virtual void move(int dx, int dy);   // move the shape +=dx and +=dy
          virtual ~Shape();

          vector<Point> points;                     // not used by all shapes
          Color lcolor;
          Line_style ls;
          Color fcolor;
};

Image

What value did we add by those extra 12 member functions and two lines of access specifications (private: and protected:)? The basic answer is that protecting the representation ensures that it doesn’t change in ways unanticipated by a class designer so that we can write better classes with less effort. This is the argument about “invariants” (§9.4.3). Here, we’ll point out such advantages as we define classes derived from Shape. One simple example is that earlier versions of Shape used

Fl_Color lcolor;
int line_style;

This turned out to be too limiting (an int line style doesn’t elegantly support line width, and Fl_Color doesn’t accommodate invisible) and led to some messy code. Had these two variables been public and used in a user’s code, we could have improved our interface library only at the cost of breaking that code (because it mentioned the names lcolor and line_style).

Image

In addition, the access functions often provide notational convenience. For example, s.add(p) is easier to read and write than s.points.push_back(p).

14.2.3 Drawing shapes

We have now described almost all but the real heart of class Shape:

void draw() const;                               // deal with color and call draw_lines
virtual void draw_lines() const;        // draw the lines appropriately

Shape’s most basic job is to draw shapes. We could remove all other functionality from Shape or leave it with no data of its own without doing major conceptual harm (see §14.4), but drawing is Shape’s essential business. It does so using FLTK and the operating system’s basic machinery, but from a user’s point of view, it provides just two functions:

• draw() applies style and color and then calls draw_lines().

• draw_lines() puts pixels on the screen.

The draw() function doesn’t use any novel techniques. It simply calls FLTK functions to set the color and style to what is specified in the Shape, calls draw_lines() to do the actual drawing on the screen, and then tries to restore color and style to what they were before the call:

void Shape::draw() const
{
          Fl_Color oldc = fl_color();
          // there is no good portable way of retrieving the current style
          fl_color(lcolor.as_int());                      // set color
          fl_line_style(ls.style(),ls.width());      // set style
          draw_lines();
          fl_color(oldc);                                      // reset color (to previous)
          fl_line_style(0);                                    // reset line style to default
}

Image

Unfortunately, FLTK doesn’t provide a way of obtaining the current style, so the style is just set to a default. That’s the kind of compromise we sometimes have to accept as the cost of simplicity and portability. We didn’t think it worthwhile to try to implement that facility in our interface library.

Note that Shape::draw() doesn’t handle fill color or the visibility of lines. Those are handled by the individual draw_lines() functions that have a better idea of how to interpret them. In principle, all color and style handling could be delegated to the individual draw_lines() functions, but that would be quite repetitive.

Image

Now consider how we might handle draw_lines(). If you think about it for a bit, you’ll realize that it would be hard for a Shape function to draw all that needs to be drawn for every kind of shape. To do so would require that every last pixel of each shape should somehow be stored in theShape object. If we kept the vector<Point> model, we’d have to store an awful lot of points. Worse, “the screen” (that is, the graphics hardware) already does that — and does it better.

Image

To avoid that extra work and extra storage, Shape takes another approach: it gives each Shape (that is, each class derived from Shape) a chance to define what it means to draw it. A Text, Rectangle, or Circle class may have a clever way of drawing itself. In fact, most such classes do. After all, such classes “know” exactly what they are supposed to represent. For example, a Circle is defined by a point and a radius, rather than, say, a lot of line segments. Generating the required bits for a Circle from the point and radius if and when needed isn’t really all that hard or expensive. SoCircle defines its own draw_lines() which we want to call instead of Shape’s draw_lines(). That’s what the virtual in the declaration of Shape::draw_lines() means:

struct Shape {
          // . . .
          virtual void draw_lines() const;        // let each derived class define its
                                                                          // own draw_lines() if it so chooses
          // . . .
};

struct Circle : Shape {
          // . . .
          void draw_lines() const;                    // “override” Shape::draw_lines()
          // . . .
};

So, Shape’s draw_lines() must somehow invoke one of Circle’s functions if the Shape is a Circle and one of Rectangle’s functions if the Shape is a Rectangle. That’s what the word virtual in the draw_lines() declaration ensures: if a class derived from Shape has defined its own draw_lines()(with the same type as Shape’s draw_lines()), that draw_lines() will be called rather than Shape’s draw_lines(). Chapter 13 shows how that’s done for Text, Circle, Closed_polyline, etc. Defining a function in a derived class so that it can be used through the interfaces provided by a base is called overriding.

Note that despite its central role in Shape, draw_lines() is protected; it is not meant to be called by “the general user” — that’s what draw() is for — but simply as an “implementation detail” used by draw() and the classes derived from Shape.

This completes our display model from §12.2. The system that drives the screen knows about Window. Window knows about Shape and can call Shape’s draw(). Finally, draw() invokes the draw_lines() for the particular kind of shape. A call of gui_main() in our user code starts the display engine.

Image

What gui_main()? So far, we haven’t actually seen gui_main() in our code. Instead we use wait_for_button(), which invokes the display engine in a more simple-minded manner.

Shape’s move() function simply moves every point stored relative to the current position:

void Shape::move(int dx, int dy)   // move the shape +=dx and +=dy
{
          for (int i = 0; i<points.size(); ++i) {
                    points[i].x+=dx;
                    points[i].y+=dy;
          }
}

Like draw_lines(), move() is virtual because a derived class may have data that needs to be moved and that Shape does not know about. For example, see Axis (§12.7.3 and §15.4).

The move() function is not logically necessary for Shape; we just provided it for convenience and to provide another example of a virtual function. Every kind of Shape that has points that it didn’t store in its Shape must define its own move().

14.2.4 Copying and mutability

Image

The Shape class declared the copy constructor and the copy assignment deleted:

Shape(const Shape&) =delete;      // prevent copying
Shape& operator=(const Shape&) = delete;

The effect is to eliminate the otherwise default copy operations. For example:

void my_fct(Open_polyline& op, const Circle& c)
{
          Open_polyline op2 = op;    // error: Shape’s copy constructor is deleted
          vector<Shape> v;
          v.push_back(c);                     // error: Shape’s copy constructor is deleted
          // . . .
          op = op2;                                // error: Shape’s assignment is deleted
}

Image

But copying is useful in so many places! Just look at that push_back(); without copying, it is hard even to use vectors (push_back() puts a copy of its argument into its vector). Why would anyone make trouble for programmers by preventing copying? You prohibit the default copy operations for a type if they are likely to cause trouble. As a prime example of “trouble,” look at my_fct(). We cannot copy a Circle into a Shape-size element “slot” in v; a Circle has a radius but Shape does not, so sizeof(Shape)<sizeof(Circle). If that v.push_back(c) were allowed, the Circle would be “sliced” and any future use of the resulting Shape element would most likely lead to a crash; the Circle operations would assume a radius member (r) that hadn’t been copied:

Image

The copy construction of op2 and the assignment to op suffer from exactly the same problem. Consider:

Marked_polyline mp {"x"};
Circle c(p,10);
my_fct(mp,c);         // the Open_polyline argument refers to a Marked_polyline

Now the copy operations of the Open_polyline would “slice” mp’s string member mark away.

Image

Basically, class hierarchies plus pass-by-reference and default copying do not mix. When you design a class that is meant to be a base class in a hierarchy, disable its copy constructor and copy assignment using =delete as was done for Shape.

Slicing (yes, that’s really a technical term) is not the only reason to prevent copying. There are quite a few concepts that are best represented without copy operations. Remember that the graphics system has to remember where a Shape is stored to display it to the screen. That’s why we “attach” Shapes to a Window, rather than copy. For example, if a Window held only a copy of a Shape, rather than a reference to the Shape, changes to the original would not affect the copy. So if we changed the Shape’s color, the Window would not notice the change and would display its copy with the unchanged color. A copy would in a very real sense not be as good as its original.

Image

If we want to copy objects of types where the default copy operations have been disabled, we can write an explicit function to do the job. Such a copy function is often called clone(). Obviously, you can write a clone() only if the functions for reading members are sufficient for expressing what is needed to construct a copy, but that is the case for all Shapes.

14.3 Base and derived classes

Image

Let’s take a more technical view of base and derived classes; that is, let us for this section (only) change the focus of discussion from programming, application design, and graphics to programming language features. When designing our graphics interface library, we relied on three key language mechanisms:

• Derivation: a way to build one class from another so that the new class can be used in place of the original. For example, Circle is derived from Shape, or in other words, “a Circle is a kind of Shape” or “Shape is a base of Circle.” The derived class (here, Circle) gets all of the members of its base (here, Shape) in addition to its own. This is often called inheritance because the derived class “inherits” all of the members of its base. In some contexts, a derived class is called a subclass and a base class is called a superclass.

• Virtual functions: the ability to define a function in a base class and have a function of the same name and type in a derived class called when a user calls the base class function. For example, when Window calls draw_lines() for a Shape that is a Circle, it is the Circle’s draw_lines()that is executed, rather than Shape’s own draw_lines(). This is often called run-time polymorphismdynamic dispatch, or run-time dispatch because the function called is determined at run time based on the type of the object used.

• Private and protected members: We kept the implementation details of our classes private to protect them from direct use that could complicate maintenance. That’s often called encapsulation.

The use of inheritance, run-time polymorphism, and encapsulation is the most common definition of object-oriented programming. Thus, C++ directly supports object-oriented programming in addition to other programming styles. For example, in Chapters 2021, we’ll see how C++ supports generic programming. C++ borrowed — with explicit acknowledgments — its key mechanisms from Simula67, the first language to directly support object-oriented programming (see Chapter 22).

That was a lot of technical terminology! But what does it all mean? And how does it actually work on our computers? Let’s first draw a simple diagram of our graphics interface classes showing their inheritance relationships:

Image

The arrows point from a derived class to its base. Such diagrams help visualize class relationships and often decorate the blackboards of programmers. Compared to commercial frameworks this is a tiny “class hierarchy” with only 16 classes, and only in the case of Open_polyline’s many descendants is the hierarchy more than one deep. Clearly the common base (Shape) is the most important class here, even though it represents an abstract concept so that we never directly make a shape.

14.3.1 Object layout

How are objects laid out in memory? As we saw in §9.4.1, members of a class define the layout of objects: data members are stored one after another in memory. When inheritance is used, the data members of a derived class are simply added after those of a base. For example:

Image

Image

A Circle has the data members of a Shape (after all, it is a kind of Shape) and can be used as a Shape. In addition, Circle has “its own” data member r placed after the inherited data members.

Image

To handle a virtual function call, we need (and have) one more piece of data in a Shape object: something to tell which function is really invoked when we call Shape’s draw_lines(). The way that is usually done is to add the address of a table of functions. This table is usually referred to as the vtbl (for “virtual table” or “virtual function table”) and its address is often called the vptr (for “virtual pointer”). We discuss pointers in Chapters 1718; here, they act like references. A given implementation may use different names for vtbl and vptr. Adding the vptr and the vtbls to the picture we get

Image

Since draw_lines() is the first virtual function, it gets the first slot in the vtbl, followed by that of move(), the second virtual function. A class can have as many virtual functions as you want it to have; its vtbl will be as large as needed (one slot per virtual function). Now when we callx.draw_lines(), the compiler generates a call to the function found in the draw_lines() slot in the vtbl for x. Basically, the code just follows the arrows on the diagram. So if x is a Circle, Circle::draw_lines() will be called. If x is of a type, say Open_polyline, that uses the vtbl exactly as Shapedefined it, Shape::draw_lines() will be called. Similarly, Circle didn’t define its own move() so x.move() will call Shape::move() if x is a Circle. Basically, code generated for a virtual function call simply finds the vptr, uses that to get to the right vtbl, and calls the appropriate function there. The cost is about two memory accesses plus the cost of an ordinary function call. This is simple and fast.

Shape is an abstract class so you can’t actually have an object that’s just a Shape, but an Open_polyline will have exactly the same layout as a “plain shape” since it doesn’t add a data member or define a virtual function. There is just one vtbl for each class with a virtual function, not one for each object, so the vtbls tend not to add significantly to a program’s object code size.

Note that we didn’t draw any non-virtual functions in this picture. We didn’t need to because there is nothing special about the way such functions are called and they don’t increase the size of objects of their type.

Defining a function of the same name and type as a virtual function from a base class (such as Circle::draw_lines()) so that the function from the derived class is put into the vtbl instead of the version from the base is called overriding. For example, Circle::draw_lines() overridesShape::draw_lines().

Image

Why are we telling you about vtbls and memory layout? Do you need to know about that to use object-oriented programming? No. However, many people strongly prefer to know how things are implemented (we are among those), and when people don’t understand something, myths spring up. We have met people who were terrified of virtual functions “because they are expensive.” Why? How expensive? Compared to what? Where would the cost matter? We explain the implementation model for virtual functions so that you won’t have such fears. If you need a virtual function call (to select among alternatives at run time), you can’t code the functionality to be any faster or to use less memory using other language features. You can see that for yourself.

14.3.2 Deriving classes and defining virtual functions

We specify that a class is to be a derived class by mentioning a base after the class name. For example:

struct Circle : Shape { /* . . . */ };

Image

By default, the members of a struct are public (§9.3), and that will include public members of a base. We could equivalently have said

class Circle : public Shape { public: /* . . . */ };

These two declarations of Circle are completely equivalent, but you can have many long and fruitless discussions with people about which is better. We are of the opinion that time can be spent more productively on other topics.

Beware of forgetting public when you need it. For example:

class Circle : Shape { public: /* . . . */ };    // probably a mistake

This would make Shape a private base of Circle, making Shape’s public functions inaccessible for a Circle. That’s unlikely to be what you meant. A good compiler will warn about this likely error. There are uses for private base classes, but those are beyond the scope of this book.

A virtual function must be declared virtual in its class declaration, but if you place the function definition outside the class, the keyword virtual is neither required nor allowed out there. For example:

struct Shape {
          // . . .
          virtual void draw_lines() const;
          virtual void move();
          // . . .
};

virtual void Shape::draw_lines() const { /* . . . */ }      // error
void Shape::move() { /* . . . */ }                                      // OK

14.3.3 Overriding

Image

When you want to override a virtual function, you must use exactly the same name and type as in the base class. For example:

struct Circle : Shape {
          void draw_lines(int) const;      // probably a mistake (int argument?)
          void drawlines() const;             // probably a mistake (misspelled name?)
          void draw_lines();                      // probably a mistake (const missing?)
          // . . .
};

Here, the compiler will see three functions that are independent of Shape::draw_lines() (because they have a different name or a different type) and won’t override them. A good compiler will warn about these likely mistakes. There is nothing you can or must say in an overriding function to ensure that it actually overrides a base class function.

The draw_lines() example is real and can therefore be hard to follow in all details, so here is a purely technical example that illustrates overriding:

struct B {
          virtual void f() const { cout << "B::f "; }
          void g() const { cout << "B::g "; }         // not virtual
};

struct D : B {
          void f() const { cout << "D::f "; }         // overrides B::f
          void g() { cout << "D::g "; }
};

struct DD : D {
          void f() { cout << "DD::f "; }                // doesn’t override D::f (not const)
          void g() const { cout << "DD::g "; }
};

Here, we have a small class hierarchy with (just) one virtual function f(). We can try using it. In particular, we can try to call f() and the non-virtual g(), which is a function that doesn’t know what type of object it had to deal with except that it is a B (or something derived from B):

void call(const B& b)
          // a D is a kind of B, so call() can accept a D
          // a DD is a kind of D and a D is a kind of B, so call() can accept a DD
{
          b.f();
          b.g();
}

int main()
{
          B b;
          D d;
          DD dd;
          call(b);
          call(d);
          call(dd);

          b.f();
          b.g();

          d.f();
          d.g();

          dd.f();
          dd.g();
}

You’ll get

B::f B::g D::f B::g D::f B::g B::f B::g D::f D::g DD::f DD::g

When you understand why, you’ll know the mechanics of inheritance and virtual functions.

Obviously, it can be hard to keep track of which derived class functions are meant to override which base class functions. Fortunately, we can get compiler help to check. We can explicitly declare that a function is meant to override. Assuming that the derived class functions were meant to override, we can say so by adding override and the example becomes

struct B {
          virtual void f() const { cout << "B::f "; }
          void g() const { cout << "B::g "; }                      // not virtual
};

struct D : B {
          void f() const override { cout << "D::f "; }      // overrides B::f
          void g() override { cout << "D::g "; }    // error: no virtual B::g to override
};

struct DD : D {
          void f() override { cout << "DD::f "; }                    // error: doesn’t override
                                                                                                  // D::f (not const)
          void g() const override { cout << "DD::g "; }       // error: no virtual D::g
                                                                                                 // to override
};

Explicit use of override is particularly useful in large, complicated class hierarchies.

14.3.4 Access

C++ provides a simple model of access to members of a class. A member of a class can be

Image

• Private: If a member is private, its name can be used only by members of the class in which it is declared.

• Protected: If a member is protected, its name can be used only by members of the class in which it is declared and members of classes derived from that.

• Public: If a member is public, its name can be used by all functions.

Or graphically:

Image

A base can also be private, protected, or public:

• If a base of class D is private, its public and protected member names can be used only by members of D.

• If a base of class D is protected, its public and protected member names can be used only by members of D and members of classes derived from D.

• If a base is public, its public member names can be used by all functions.

These definitions ignore the concept of “friend” and a few minor details, which are beyond the scope of this book. If you want to become a language lawyer you need to study Stroustrup, The Design and Evolution of C++ and The C++ Programming Language, and the ISO C++ standard. We don’t recommend becoming a language lawyer (someone knowing every little detail of the language definition); being a programmer (a software developer, an engineer, a user, whatever you prefer to call someone who actually uses the language) is much more fun and typically much more useful to society.

14.3.5 Pure virtual functions

Image

An abstract class is a class that can be used only as a base class. We use abstract classes to represent concepts that are abstract; that is, we use abstract classes for concepts that are generalizations of common characteristics of related entities. Thick books of philosophy have been written trying to precisely define abstract concept (or abstraction or generalization or . . .). However you define it philosophically, the notion of an abstract concept is immensely useful. Examples are “animal” (as opposed to any particular kind of animal), “device driver” (as opposed to the driver for any particular kind of device), and “publication” (as opposed to any particular kind of book or magazine). In programs, abstract classes usually define interfaces to groups of related classes (class hierarchies).

Image

In §14.2.1, we saw how to make a class abstract by declaring its constructor protected. There is another — and much more common — way of making a class abstract: state that one or more of its virtual functions needs to be overridden in some derived class. For example:

class B {                                      // abstract base class
public:
          virtual void f() =0;         // pure virtual function
          virtual void g() =0;
};

B b;                                           // error: B is abstract

The curious =0 notation says that the virtual functions B::f() and B::g() are “pure”; that is, they must be overridden in some derived class. Since B has pure virtual functions, we cannot create an object of class B. Overriding the pure virtual functions solves this “problem”:

class D1 : public B {
public:
          void f() override;
          void g() override;
};

D1 d1;                                        // OK

Note that unless all pure virtual functions are overridden, the resulting class is still abstract:

class D2 : public B {
public:
          void f() override;
          // no g()
};

D2 d2;          // error: D2 is (still) abstract

class D3 : public D2 {
public:
          void g() override;
};

D3 d3;          // OK

Image

Classes with pure virtual functions tend to be pure interfaces; that is, they tend to have no data members (the data members will be in the derived classes) and consequently have no constructors (if there are no data members to initialize, a constructor is unlikely to be needed).

14.4 Benefits of object-oriented programming

Image

When we say that Circle is derived from Shape, or that Circle is a kind of Shape, we do so to obtain (either or both)

• Interface inheritance: A function expecting a Shape (usually as a reference argument) can accept a Circle (and can use a Circle through the interface provided by Shape).

• Implementation inheritance: When we define Circle and its member functions, we can take advantage of the facilities (such as data and member functions) offered by Shape.

Image

A design that does not provide interface inheritance (that is, a design for which an object of a derived class cannot be used as an object of its public base class) is a poor and error-prone design. For example, we might define a class called Never_do_this with Shape as its public base. Then we could override Shape::draw_lines() with a function that didn’t draw the shape, but instead moved its center 100 pixels to the left. That “design” is fatally flawed because even though Never_do_this provides the interface of a Shape, its implementation does not maintain the semantics (meaning, behavior) required of a Shape. Never do that!

Image

Interface inheritance gets its name because its benefits come from code using the interface provided by a base class (“an interface”; here, Shape) and not having to know about the derived classes (“implementations”; here, classes derived from Shape).

Image

Implementation inheritance gets its name because the benefits come from the simplification in the implementation of derived classes (e.g., Circle) provided by the facilities offered by the base class (here, Shape).

Note that our graphics design critically depends on interface inheritance: the “graphics engine” calls Shape::draw() which in turn calls Shape’s virtual function draw_lines() to do the real work of putting images on the screen. Neither the “graphics engine” nor indeed class Shape knows which kinds of shapes exist. In particular, our “graphics engine” (FLTK plus the operating system’s graphics facilities) was written and compiled years before our graphics classes! We just define particular shapes and attach() them to Windows as Shapes (Window::attach() takes a Shape&argument; see §E.3). Furthermore, since class Shape doesn’t know about your graphics classes, you don’t need to recompile Shape each time you define a new graphics interface class.

Image

In other words, we can add new Shapes to a program without modifying existing code. This is a holy grail of software design/development/maintenance: extension of a system without modifying it. There are limits to which changes we can make without modifying existing classes (e.g.,Shape offers a rather limited range of services), and the technique doesn’t apply well to all programming problems (see, for example, Chapters 1719 where we define vector; inheritance has little to offer for that). However, interface inheritance is one of the most powerful techniques for designing and implementing systems that are robust in the face of change.

Image

Similarly, implementation inheritance has much to offer, but it is no panacea. By placing useful services in Shape, we save ourselves the bother of repeating work over and over again in the derived classes. That can be most significant in real-world code. However, it comes at the cost that any change to the interface of Shape or any change to the layout of the data members of Shape necessitates a recompilation of all derived classes and their users. For a widely used library, such recompilation can be simply infeasible. Naturally, there are ways of gaining most of the benefits while avoiding most of the problems; see §14.3.5.

Image Drill

Unfortunately, we can’t construct a drill for the understanding of general design principles, so here we focus on the language features that support object-oriented programming.

1. Define a class B1 with a virtual function vf() and a non-virtual function f(). Define both of these functions within class B1. Implement each function to output its name (e.g., B1::vf()). Make the functions public. Make a B1 object and call each function.

2. Derive a class D1 from B1 and override vf(). Make a D1 object and call vf() and f() for it.

3. Define a reference to B1 (a B1&) and initialize that to the D1 object you just defined. Call vf() and f() for that reference.

4. Now define a function called f() for D1 and repeat 1–3. Explain the results.

5. Add a pure virtual function called pvf() to B1 and try to repeat 1–4. Explain the result.

6. Define a class D2 derived from D1 and override pvf() in D2. Make an object of class D2 and invoke f(), vf(), and pvf() for it.

7. Define a class B2 with a pure virtual function pvf(). Define a class D21 with a string data member and a member function that overrides pvf(); D21::pvf() should output the value of the string. Define a class D22 that is just like D21 except that its data member is an int. Define a function f() that takes a B2& argument and calls pvf() for its argument. Call f() with a D21 and a D22.

Review

1. What is an application domain?

2. What are ideals for naming?

3. What can we name?

4. What services does a Shape offer?

5. How does an abstract class differ from a class that is not abstract?

6. How can you make a class abstract?

7. What is controlled by access control?

8. What good can it do to make a data member private?

9. What is a virtual function and how does it differ from a non-virtual function?

10. What is a base class?

11. What makes a class derived?

12. What do we mean by object layout?

13. What can you do to make a class easier to test?

14. What is an inheritance diagram?

15. What is the difference between a protected member and a private one?

16. What members of a class can be accessed from a class derived from it?

17. How does a pure virtual function differ from other virtual functions?

18. Why would you make a member function virtual?

19. Why would you make a virtual member function pure?

20. What does overriding mean?

21. How does interface inheritance differ from implementation inheritance?

22. What is object-oriented programming?

Terms

abstract class

access control

base class

derived class

dispatch

encapsulation

inheritance

mutability

object layout

object-oriented override

polymorphism

private

protected

public

pure virtual function

subclass

superclass

virtual function

virtual function call

virtual function table

Exercises

1. Define two classes Smiley and Frowny, which are both derived from class Circle and have two eyes and a mouth. Next, derive classes from Smiley and Frowny which add an appropriate hat to each.

2. Try to copy a Shape. What happens?

3. Define an abstract class and try to define an object of that type. What happens?

4. Define a class Immobile_Circle, which is just like Circle but can’t be moved.

5. Define a Striped_rectangle where instead of fill, the rectangle is “filled” by drawing one-pixel-wide horizontal lines across the inside of the rectangle (say, draw every second line like that). You may have to play with the width of lines and the line spacing to get a pattern you like.

6. Define a Striped_circle using the technique from Striped_rectangle.

7. Define a Striped_closed_polyline using the technique from Striped_rectangle (this requires some algorithmic inventiveness).

8. Define a class Octagon to be a regular octagon. Write a test that exercises all of its functions (as defined by you or inherited from Shape).

9. Define a Group to be a container of Shapes with suitable operations applied to the various members of the Group. Hint: Vector_ref. Use a Group to define a checkers (draughts) board where pieces can be moved under program control.

10. Define a class Pseudo_window that looks as much like a Window as you can make it without heroic efforts. It should have rounded corners, a label, and control icons. Maybe you could add some fake “contents,” such as an image. It need not actually do anything. It is acceptable (and indeed recommended) to have it appear within a Simple_window.

11. Define a Binary_tree class derived from Shape. Give the number of levels as a parameter (levels==0 means no nodes, levels==1 means one node, levels==2 means one top node with two sub-nodes, levels==3 means one top node with two sub-nodes each with two sub-nodes, etc.). Let a node be represented by a small circle. Connect the nodes by lines (as is conventional). P.S. In computer science, trees grow downward from a top node (amusingly, but logically, often called the root).

12. Modify Binary_tree to draw its nodes using a virtual function. Then, derive a new class from Binary_tree that overrides that virtual function to use a different representation for a node (e.g., a triangle).

13. Modify Binary_tree to take a parameter (or parameters) to indicate what kind of line to use to connect the nodes (e.g., an arrow pointing down or a red arrow pointing up). Note how this exercise and the last use two alternative ways of making a class hierarchy more flexible and useful.

14. Add an operation to Binary_tree that adds text to a node. You may have to modify the design of Binary_tree to implement this elegantly. Choose a way to identify a node; for example, you might give a string "lrrlr" for navigating left, right, right, left, and right down a binary tree (the root node would match both an initial l and an initial r).

15. Most class hierarchies have nothing to do with graphics. Define a class Iterator with a pure virtual function next() that returns a double* (see Chapter 17). Now derive Vector_iterator and List_iterator from Iterator so that next() for a Vector_iterator yields a pointer to the next element of a vector<double> and List_iterator does the same for a list<double>. You initialize a Vector_iterator with a vector<double> and the first call of next() yields a pointer to its first element, if any. If there is no next element, return 0. Test this by using a function void print(Iterator&) to print the elements of a vector<double> and a list<double>.

16. Define a class Controller with four virtual functions on(), off(), set_level(int), and show(). Derive at least two classes from Controller. One should be a simple test class where show() prints out whether the class is set to on or off and what is the current level. The second derived class should somehow control the line color of a Shape; the exact meaning of “level” is up to you. Try to find a third “thing” to control with such a Controller class.

17. The exceptions defined in the C++ standard library, such as exception, runtime_error, and out_of_range (§5.6.3), are organized into a class hierarchy (with a useful virtual function what() returning a string supposedly explaining what went wrong). Search your information sources for the C++ standard exception class hierarchy and draw a class hierarchy diagram of it.

Postscript

Image

The ideal for software is not to build a single program that does everything. The ideal is to build a lot of classes that closely reflect our concepts and that work together to allow us to build our applications elegantly, with minimal effort (relative to the complexity of our task), with adequate performance, and with confidence that the results produced are correct. Such programs are comprehensible and maintainable in a way that code that was simply thrown together to get a particular job done as quickly as possible is not. Classes, encapsulation (as supported by private andprotected), inheritance (as supported by class derivation), and run-time polymorphism (as supported by virtual functions) are among our most powerful tools for structuring systems.