4.11 Other Class Features
Five additional features of a class are:
The PolyShape class is an example of a class possessing several very small methods. Each of the Up, Down, Right, and Left methods are exactly one line of code and, in each case, the one line of code simply increments one of the private data elements of the class. For a PolyShape object, p, an invocation of
results in the compiler generating assembly language code of the form
save registers on the stack establish new call frame push argument(s) on the stack call p.Up remove call frame restore saved registers
As can be seen, there is overhead involved in setting up the execution environment to perform the method and to restore the execution environment after the method has been executed. For small methods, this overhead may be disproportionate to the execution time of the method itself. For the four methods in the PolyShape class, the execution time of the single instruction may be a single instruction cycle. The call instruction leads to the execution that is "out of line" because it changes the program counter to jump to a new piece of code.
When the efficiency of small methods is a concern, inline functions may be used to allow the compiler to generate more efficient code than would normally be the case. For an inline method the call
would result in the compiler generating the following code:
p.currentY = p.currentY - 10;
This code is generated "in line" with the other code of the caller. The overhead of a method invocation is entirely removed. But because this efficiency comes at some price (as will be seen later) the decision to use inline functions should be made carefully.
The figure below shows the redefinition of the PolyShape class with the Up, Down, Right, and Left methods represented as inline methods. Notice that the code for each of the four methods has been included immediately after the definition of each method in the class definition (in the header file). This placement of the code is a suggestion to the compiler to generate inline code for all invocations of these methods. Notice that the other methods of the class are not inline methods.
Because the code for the inline methods is given in the header file, it must not appear in the code file.
Inline methods do not violate the encapsulation properties of the class, for programmers using a class with inline methods are still prohibited from direct access to the private data of a PolyShape object. However, as an optimization for inline methods, the compiler can generate the machine-level code as efficiently as if the programmer were able to do this. Thus, inline methods improve the execution time efficiency of code without sacrificing the encapsulation properties of the class.
Inline methods are a mixed blessing and thus should be used carefully because:
Finally, it must be kept in mind that an inline method is only a suggestion to a compiler. In some cases the compiler may refuse to inline methods that it considers too complicated to inline correctly or easily.
In some cases, an object may need to invoke a method of another object in order to pass itself as an argument of the method or, in the example to be considered here, an object may want to return itself as the result of one of its own methods. The question is, how is an object able to refer to itself? The PolyShape class will be used to illustrate why an object may be designed to return itself as the result of a method. The code below shows a typical use of the PolyShape class to draw a polygonal shape. Notice that sequences of statements are needed to perform each operation one at a time.
Instead of using sequences of statements, it might be desirable to provide an interface such that related operations can be strung together. An example of this has already been seen in the case of the stream I/O operators that can be perfomed one after the other in a single statement. This clearly makes the use of these operators more natural and readable. The PolyShape class can be redesigned to allow the code above to be written as shown in the figure below.
Although expressions such as "quad.Left(20).Mark()" may seem strange at first, they are no more unusual than arithmetic expressions like "a + b - c". In each case the subexpression yields a result (an object) that is the subject of the next operation to be performed.
The existing definition of the PolyShape class will not allow its methods to be applied in sequence in one expression. The methods Up, Down, Right, and Left all return "void" as a result. Thus, an expression like
What is needed is a way for each of the operations Up, Down, Left,
and Right to return as a result the same object on which they
themselves operated. This involves changes in both the definition
and implementation of the PolyShape class. These changes are shown
in the code below.
The methods being changed return a result of type PolyShape&. The intent of the change would be defeated if the return type were simply PolyShape because this would indicate return by-copy. If the result were returned by copy, each operation in a sequence would operate on a different copy of the object. This is contrary to the intent of applying all operations in a sequence to the same object.
The methods of an object may refer to a predefined, special variable named "this," which is a pointer to the object itself. This special variable is understood by the compiler to be correctly typed for the current object. This means that in a method of a PolyShape class the type of "this" is taken to be "PolyShape*" (i.e., a pointer to a PolyShape object). In a method of a Rectangle class the type of "this" is taken to be "Rectangle*" (i.e., a pointer to a Rectangle object). Note that the "this" variable is predefined; it should not (and indeed cannot) be declared in the program.
The role of the "this" pointer is seen in the code for the revised Up method. After changing the state variables of the object, the code must be able to indicate that the return value is the same PolyShape object as that which the current method is operating upon. The "this" pointer can be used for this purpose. Since the return type is an object, not a pointer to an object, the return value is "*this" (i.e., the object to which "this" points") and not "this" (a pointer to an object).
It is not uncommon that two or more methods in the public interface of a class contain repeated code. Typical examples of repeated code are methods that use the same subalgorithm to compute an intermediate value or that use the same code to perform error checking. The Rectangle class is an example of repeated code. As shown in the figure below, the Rectangle constructor and the various move methods both perform the same manipulations of the four aggregated Location objects.
To remove repeated code it is necessary to define a new method (member function of the class) that can be called by the methods in the public interface without the new method itself becoming part of the public interface.
The abbreviated revised definition of the Rectangle class shown below contains a private method named AdjustCorners(). This method assumes that the upperLeft Location has been set to its correct value. Using the upperLeft and the area, AdjustCorners sets the positions the other corners of the Rectangle to their correct Locations.
Since the private method is declared in the private part of the Rectangle class, it can be accessed (i.e., invoked) by any of the methods in the Rectangle class and only from methods in the Rectangle class. The implementation of the AdjustCorners private method and the revised MoveUp method are shown in the code below.
In this revised code for the Rectangle class notice that the AdjustCorners method is like every other method in the class in that AdjustCorners can access the private data of the Rectangle class, and in the code file its fully qualified name is Rectangle::AdjustCorners
The only difference between AdjustCorners and the other methods
is that AdjustCorners is not visible outside of the class. Thus,
the following code:
Rectangle rect(Location(50,50), Shape(20,20));...rect.AdjustCorners();
would be in error because it attempts to access a private part, in this case a private method, of the Rectangle class. An appropriate error message would be generated for this code by the compiler.
From within the Rectangle class the AdjustCorners method can be invoked in either of two ways:
In the first way the object to which the method is applied is the current object. The this pointer is assumed implicitly. In the second way the this pointer is used to explicitly indicate the current object. Both ways are correct and result in the same invocation.
Static data is way of creating data elements shared among all objects of a given class. The role of this kind of data is more clearly reflected in the names "class data" or "class-wide data," which are sometimes used to describe what C++ calls static data. Static data can be used whenever the attribute of an abstraction should apply to each and every instance of that abstraction.
To illustrate the use of static data, the Rectangle class is extended with a color attribute so that all Rectangles are always drawn with the same color. This revised definition of the Rectangle class is shown below; note its new private data member that defines the color of the Rectangle. Because this data member is declared as static, the value of this data member is shared by all objects of the Rectangle class. Also added to the class is a method to set the color to a new value.
The class-wide effect of a static variable is illustrated by the following code. Suppose that the static variable rectangleColor has been initialized so that all Rectangles are initially drawn in a red color. The first time that the two Rectangles are drawn they will appear in red. Once the color is changed to blue, then all Rectangles drawn after that will be drawn in blue. Notice that the color is changed using the setColor method for rect2. However, this changes the color not just for that Rectangle object, but for all Rectangle objects. Thus, when the two Rectangles are drawn again after the change in color, both of the Rectangles will be drawn in blue.
Rectangle rect1(...); // constructor details not shown Rectangle rect2(...); Canvas canvas(...); Color blue(0, 200,0); //a shade of blue ... rect1.Draw(canvas); // will be drawn in red rect2.Draw(canvas); // will be drawn in red ... rect2.setColor(blue); // change color ... rect2.Draw(canvas); // will be drawn in blue rect1.Draw(canvas); // will be drawn in blue
Because a static class variable is shared among all objects of a given class, its value cannot be initialized by the constructor for that class. If this were done, then every time a new object of the class is created, the value of the static class variable would be set back to its initial value. Instead, a means is needed to initialize the static class variable one time regardless of how objects of the class are created or when these objects are created. To guarantee that the static class variable is initialized only once, the initialization code is placed with the code (the implementation) of the class. For the Rectangle class, the initialization is shown in the figure below.
Notice that the fully qualified name (Rectangle::rectangleColor) must be used in naming the variable to be initialized. Also notice that that the setColor method uses the simple name (rectangleColor) of this variable.
In limited, special cases, there is a need to establish a special relationship between two classes for efficiency or security reasons by making one class a "friend" of another. If class B declares that class A is a friend class, then the methods of class A are allowed to access the private section of class B. The friend declaration must be made in the class whose private section is being accessed (i.e., a class can grant access to its private section). The friend relationship is asymmetic: class A is a friend of class B does not imply that class B is a friend of class A. The friend relationship is not transitive: if class A is a friend of class B and class B is a friend of class C, class A is not automatically a friend of class C. A friend relationship between two classes devolves upon the objects of these classes: if class B declares that class A is a friend class then all objects of class A can access the private data of any object of class B that the class A object knows about.
Clearly the notion of a friend relationship must be used with great restraint because it weakens the encapulation of the class granting the friendship, for its usual safeguards and privacy that accompany encapsulation are lessened. However, the friend relation establishes only a selective weakening of encapsulation and, if used modestly, can serve useful purposes. Used to an extreme, the friend relation becomes dangerous and corrupts a good design.
Two examples of good uses of the friend relation follow. The first example involves a manager class that requires special access to the encapsulated data of a PolyShape class. In this example, efficiency motivates using the friend relationship. The second example shows how to use the friend relation to insure that Rectangle objects can only be created in certain ways. In this example, access control is the motivation for using the friend relationship.
The first example of using the friend relation involves an application in which a number of Rectangle objects exist within a space that is represented by an object of a Space class. In different applications, the Space could model physical objects moving within a two dimensional scene, windows and other icons on a computer screen, the placement of electronic components on a board, and many others. A critical operation typically associated with such a Space class is an intersection test, that is, to determine if any two objects in the space overlap. Computing the intersection relies on knowledge of the vertices and/or edges of each of the shapes within the space. The intersection test can be time consuming if there are many objects in the space so there might be some concern for making this test as efficient as possible. Given the definition of the Rectangle class, there is no way for the Space class to access the set of Location objects that are part of the private data of a Rectangle object. One alternative is to add an accessor method to the Rectangle class that would return one or all of the Location objects of a PolyShape object. This approach is sometimes objectionable because it exposes the internal data of the Rectangle class to all other classes, not just to the Space class. The RectangleFactory class is shown in Code Segment 4-37. A second alternative uses the friend relationship: the Rectangle object declares that the Space class is a friend class. This relationship allows the Space class to access the private data (the Location information) inside of the Rectangle class. The outline of the code that establishes the friend relationship between the Rectangle class and the Space class is shown below.
The friend declaration is placed in the class granting the access (in this case the Rectangle class). The effect of the friend declaration can be seen in the skeleton code shown for the AnyInteresections() method, in which the private data (upperLeft, lowerRight) of the Rectangle objects are directly accessed via pointers to the two Rectangle objects. In the absence of the friend declaration, these direct accesses would be detected by the compiler as illegal.
The second example uses the friend relationship to obtain enhanced access control. Consider an application that is using Rectangles, all of which are required to be of the same Shape. Such a restriction comes from the problem domain in the form of requirements like all tiles are the same shape, all windows in this building are the same shape, or all memory chips on this layout are the same shape. To enforce the requirement of identical Shape we might consider creating a RectangleFactory class that constructs and returns Rectangle objects. The RectangleFactory is constructed with a Shape object that it will use during its lifetime. All Rectangle objects produced by a given RectangleFactory will have the same Shape. Recall that the Rectangle class does not provide a method for the Shape to be changed. Therefore, exclusive use of the RectangleFactory to create Rectangle objects enforces the application requirement.
The RectangleFactory may be used to produce Rectangle objects as follows:
RectangleFactory squareFactory(Shape(50,50)); Canvas canvas(...); ... Rectangle *rect1, *rect2; ... rect1 = squareFactory(Location(100,150)); rect2 = squareFactory(Location(200,150)); ... rect1->Draw(canvas); rect2->Draw(canvas);
In this code, a RectangleFactory object is constucted so that all Rectangle objects that it produces will have a square shape that is 50 x 50. Two Rectangle objects are produced using the squareFactory. The objects produced by the squareFactory are normal Rectangle objects as shown by the way rect1 and rect2 can be manipulated using the standard Rectangle interface.
The RectangleFactory design, however, cannot truly enforce the requirement that all Rectangles have the same shape; only those Rectangles that the factory produces will have this property. There is nothing in the design of the RectangleFactory that would prevent a programmer from bypassing the RectangleFactory and directly instantiating a Rectangle object.
The friend relationship can be used to endow the RectangleFactory with the ability to truly enforce its design requirement, by modifying the Rectangle class as shown below. Two changes are made in the Rectangle class. First, the RectangleFactory is declared as a friend of the Rectangle class. As before, this allows the RectangleFactory to access the private section of the Rectangle class. Second, the Rectangle class's constructor is moved to the private section of the Rectangle class. These two changes have the desired effect because any code that attempts to construct a Rectangle object must have access to the private section of the Rectangle class, for that is where the constructor resides. However, only the Rectangle class itself and the RectangleFactory class have access to the private section of the Rectangle class. Thus, any attempt to construct a Rectangle object outside of these two classes will be detected by the compiler as an error. Notice that pointers to Rectangles (Rectangle*) can still be created outside of these two classes because the pointer is not a Rectangle object.
In this example, access to the constructor for a class was restricted to the named friend class. However, the same technique may be used to restrict other operations on a class-by-class basis.