4.11 Other Class Features


Five additional features of a class are:

  • inline methods : an efficient way to implement very small methods
  • this pointer : a way for an object to refer to itself
  • private methods : utility methods that are useful in implementing a class, but are not intended to be part of the class's public interface
  • static class variables : data that is shared among all objects of a given class
  • friend classes : allow special access to the private part of a class for performance or access-control reasons

Each of these features is illustrated with an example. The examples are variations of the Rectangle class or the PolyShape class introduced in section 4.5 and 4.7, respectively.

Inline Methods

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

       p.Up(10)

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

       p.Up(10) 

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.


PolyShape Class with Inline Methods
class PolyShape
{
private:
   LocationNode *head;
   LocationNode *tail;
   int currentX, currentY;
   int length;

public:
    PolyShape(int x, int y); 
    void Up   (int n){currentY = currentY - n;}; 
    void Down (int n){currentY = currentY + n;};
    void Left (int n){currentX = currentX - n;};
    void Right(int n){currentX = currentX + n;};
    void Mark();
    void Draw(Canvas& canvas);
    ~PolyShape();
};

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:

  • The presence of the code in the header file conveys information about how the class is implemented and this violates the spirit, though not the letter, of the law of encapsulation. Programmers may attempt to make their own code as efficient as possible by clever programming tricks that exploit the implementation information revealed by the inline methods. However, if the code of the class changes - and, hence, the code of the inline methods also changes - the code with the tricks may be worse than a straighforward use of the class.

  • If the code of an inline method changes, all code that uses this method must also be recompiled. This is in contrast to a regular (not inline) method. If the code of a regular method in a class is changed, only the code for that class needs to be recompiled. Thus, inline methods cause more work at compile time in exchange for some efficiency gain at run-time. It is important to know whether this trade-off is worthwhile before using inline methods.

  • If an inline method is made into a regular method or a regular method is inlined, all code that uses this method must be recompiled, even if the method's code is unchanged. This recompilation is necessary because the code generated for the user of the method is different depending on whether or not the method is inline.

  • There is a performance penalty to be paid for inappropriate use of inlined methods. One of the advantages of using a method call, as opposed to inline code, is that the method's code is not duplicated. If a method that has many lines of code is inlined and is called from many places in the system, then the code for that method is duplicated at every place where a call to the method occurs. This causes the size of the code of the system to grow. The proper use of inline methods should evaluate the possible reduction in execution time gained by the inlining against the possible increase in the size of the code. Even though memories are large, increases in code size can affect the behavior of hardware code caches and system paging performance.

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.


The "this" Pointer

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.



Using the PolyShape Class
  PolyShape quad(20,20);   
   ...   

   quad.Right(100);
   quad.Down(50);
   quad.Mark();
   quad.Left(20);
   quad.Mark();
   quad.Down(30);
   quad.Left(50);
   quad.Mark();
   quad.Draw(canvas);
   

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.


Revising the PolyShape Class to
Allow a Sequence of Method Calls
  PolyShape quad(20,20); 
   ... 

   quad.Right(100).Down(50).Mark();
   quad.Left(20).Mark();
   quad.Down(30).Left(50).Mark();
   quad.Draw(canvas);

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



     quad.Left(20).Mark();


cannot be done because the result of "quad.Left(20)" would be void and the subsequent expression would be an illegal "void.Mark()".

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.


Using the "this" Pointer in the PolyShape Class
class PolyShape
{
private:
   // same as before

public:
    ...
    PolyShape& Up   (int n); 
    PolyShape& Down (int n);
    PolyShape& Left (int n);
    PolyShape& Right(int n);
    PolyShape& Mark();
    
};

// a portion of the implementation
PolyShape& PolyShape::Up(int n)
{ currentY = currentY - n;
  return *this;
}

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

Private Methods

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.


Repeated Code in the Rectangle Class
Rectangle::Rectangle(Location corner, Shape shape)
{ area = shape;
  upperLeft  = corner;
  upperRight = Location(upperLeft.Xcoord() + area.Width(),
                        upperLeft.Ycoord());
  lowerLeft  = Location(upperLeft.Xcoord() ,
                        upperLeft.Ycoord() + area.Height());
  lowerRight = Location(upperLeft.Xcoord() + area.Width(),
                        upperLeft.Ycoord() + area.Height());
}

void Rectangle::MoveUp(int deltaY)
{ upperLeft  = Location(upperLeft.Xcoord(),
                        upperLeft.Ycoord() + deltaY);
  upperRight = Location(upperLeft.Xcoord() + area.Width(),
                        upperLeft.Ycoord());
  lowerLeft  = Location(upperLeft.Xcoord() ,
                        upperLeft.Ycoord() + area.Height());
  lowerRight = Location(upperLeft.Xcoord() + area.Width(),
                        upperLeft.Ycoord() + area.Height());
}
// ... MoveDown, MoveLeft, MoveRight similar to MoveUp

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.


Private Method Defined in the Rectangle Class
class Rectangle{ private:Location upperLeft;// other Locations...Shape    area;    void AdjustCorners(); // private methodpublic:Rectangle (Location corner, Shape shape);void MoveUp   (int deltaY);// ... other Move methodsvoid Draw(Canvas& canvas);void Clear(Canvas& canvas);~Rectangle();};

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.


Implementation of Revised Rectangle Class
void Rectangle::AdjustCorners()
{ upperRight = Location(upperLeft.Xcoord() + area.Width(),
                        upperLeft.Ycoord());
  lowerLeft  = Location(upperLeft.Xcoord() ,
                        upperLeft.Ycoord() + area.Height());
  lowerRight = Location(upperLeft.Xcoord() + area.Width(),
                        upperLeft.Ycoord() + area.Height());
}

Rectangle::Rectangle(Location corner, Shape shape)
{ area = shape;
  upperLeft  = corner;
  AdjustCorners();  // call private method
}

void Rectangle::MoveUp(int deltaY)
{ upperLeft  = Location(upperLeft.Xcoord(),
                        upperLeft.Ycoord() + deltaY);
  AdjustCorners();  // call private method
}
// ... MoveDown, MoveLeft, MoveRight similar to MoveUp

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:

  • Simply as AdjustCorners() as shown above
  • By using the this pointer as this->AdjustCorners()

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 Class Variables

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.


Static Data Declared in the Rectangle Class
class Rectangle{ private:Location upperLeft;Location upperRight;Location lowerLeft;Location lowerRight;Shape    area;   static Color rectangleColor; // class variablepublic:Rectangle (Location corner, Shape shape);void setColor(Color newColor);//.. other methods as before~Rectangle();};

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.


Initializing Static Class Variables
Color Rectangle::rectangleColor = Color(200,0,0); //a shade of redvoid Rectangle::AdjustCorners()
{ //...as before 
}
Rectangle::Rectangle(Location corner, Shape shape)
{ // ... as before
}
void Rectangle::setColor(Color color){ rectangleColor = color; // change for all Rectangle objects}// all other methods as before

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.

Friend Classes

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.


Using Friend Classes for Efficiency
class Rectangle{ private:Location upperLeft;Location upperRight;Location lowerLeft;Location lowerRight;Shape    area;   friend class Space;  // allow the Space class                     // access to the private datapublic:Rectangle (Location corner, Shape shape);//.. other methods as before~Rectangle();};class Space{ private:// some data structure to maintain a   // set of Rectangle objectspublic:Space();int AnyIntersections();  // determine if there are                        // any intersections// ...other methods};// in implementation of Space classint Space::AnyIntersections(){ Rectangle *rect1;Rectangle *rect2;// use data structure to locate two Rectangle objects// rect1 and rect2if (... rect1->upperLeft.Xcoord() ...       rect2->lowerRight.Ycoord() ... )//...}

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 Rectangle Factory Class
class RectangleFactory
{
  private:
    Shape commonShape;

  public:
   RectangleFactory(Shape shape);
   Rectangle& New(Location loc);
   ~Rectangle();
};

RectangleFactory::RectangleFactory(Shape shape)
{ commonShape = shape;
}

Rectangle* RectangleFactory::New(Location loc)
{ return new Rectangle(loc,commonShape);
}

RectangleFactory::~RectangleFactory(){}

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.


Using the Friend Relationship to Enforce Access Control
class Rectangle
{
  private:
   // data as before

 friend class RectangleFactory;Rectangle(Location loc, Shape shape);
 public:
   // other methods as before
};

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.




©1998 Prentice-Hall, Inc.
A Simon & Schuster Company
Upper Saddle River, New Jersey 07458

Legal Statement

 

ÿ