Graphics Class Design : Shape

Chapter 14: Graphics Class Design

"Functional, durable, beautiful."

Shape 14.2

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 and a Color (for lines and 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 
      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;

      virtual ~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;
      vector<Point> points;               // not used by all shapes
      Color lcolor;                      // color for lines and characters
      Line_style ls; 
      Color fcolor;                      // fill color

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

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.

An abstract class 14.2.1

Consider first Shape's constructor:


The constructor is protected. That means that it 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

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 constructor is defined like this:

      : lcolor(fl_color()),            // default color for lines and characters
      ls(0),                           // default style
      fcolor(Color::invisible)      // no fill

It is a default constructor, so it sets the members to their default. 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 defaults to an empty vector.

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 B'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 abstract and 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-3 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-3" 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 B'17.5.2, where we show a use.

Access control 14.2.2

Class Shape declares all data members private:

      Vector<Point> points;
      Color lcolor;
      Line_style ls;
      Color fcolor;

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 (B'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

Naturally, points start 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 member of a class public is bad design. To others, our design seems overly restrictive because we don't allow direct write access to the members 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? (B'12.7.6)
  • Lines allows only pairs of points to be added (and not an individual point; B'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 (B'13.8).

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_points() in our example classes and code, so set_point() is there, 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)

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 program? 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 
      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

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

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" (B'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 line_color and line_style).

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).

Graphics Class Design : Shape

Drawing shapes 14.2.3

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 not remove all other functionality from Shape and leave it with no data of its own without doing major conceptual harm (see B'14.4); 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 the color and shape 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
      fl_color(oldc);      // reset color (to previous)
      fl_line_style(0);      // reset line style to default

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_line() 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_line() functions, but that would be quite repetitive.

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 the Shape object. If we kept the vector 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.

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 each 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. So Circle 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 B'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.

Figure 14.1

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 simpleminded 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) {

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 (B'12.7.3 and B'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().

Copying and mutability 14.2.4

The Shape class declared the copy constructor and the copy assignment operator private:

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

The effect is that only members of Shape can copy objects of Shape using the default copy operations. That is a common idiom for preventing accidental copying. For example:

void my_fct(const Open_polyline& op, const Circle& c)
      Open_polyline op2 = op;   // error: Shape's copy constructor is private
      Vector<Shape> v;
      v.push_back(c);               // error: Shape's copy constructor is private
      // . . .
      op = op2;                     // error: Shape's assignment is private

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-sized 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:

Figure 14.2

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.

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 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. The Window would know nothing about a copy, so a copy would in a very real sense not be as good as its original.

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.

"This content is an excerpt from Chapter 14, "Graphics Class Design", from Bjarne Stroustrup's new book, "Programming: Principles and Practice Using C++", published by Addison-Wesley Professional, ISBN 0321543726, Copyright 2009 Pearson Education, Inc. To see uses of this code, please refer to Chapter 12 which is available for free sample download on the publisher site: http://www.informit.com/content/images/9780321543721/samplepages/0321543726_Sample.pdf. For more info on the book, including a full Table of Contents please visit: www.informit.com/title/0321543726

B) Copyright Pearson Education. All rights reserved.

This article was originally published on February 5th, 2009
Close Icon
Thanks for your registration, follow us on our social networks to keep up-to-date