8.5 Type Conversion and Operator Overloading



Type conversions are often provided to lessen the duplication that frequently occurs when overloading operators. A typical example of this duplication is encountered in defining binary, commutative operators (such as + and *). Suppose that an overloaded set of arithmetic operators is needed in class X so that an int may be added to an object of class X. This requires two operator overloadings: one for the case of "X + int" and one for the case of "int + X." To eliminate this duplication, a type conversion is defined that converts an int to an X and provides a single overloading for the case "X + X." Thus, both "int + X" and "X + int" become the single case "X + X".

A set of classes for manipulating colors will be used in this section to illustrate type conversion and operator overloading. Colors generated by a computer are a combination of three primary colors (red, blue, and green). Each of these colors has an intensity value associated with it. The range of intensity values determines how many different colors may be produced. If we assume that the intensities are in the range 0-63, then the color aqua is generated by using no red and intensities of 50 for both green and blue. The color purple is obtained from aqua by adding red at intensity 63 and increasing the blue intensity by 13.

Operator overloading makes it possible to program with colors in the most natural way. The phrase "add red at intensity nine to aqua" suggests using operator overloading to create a set of arithmetic operators that could be used as follows:

  Color aqua(Red(0), Green(50), Blue(50));

   Color darkRed = Red(63);
   Color purple  = darkRed + aqua + Blue(13);
   Color peach   = purple - Blue(13) - Green(3);

   Color test1 = darkRed + peach;
   Color test2 = peach + darkRed;

   cout << aqua    << endl;
   cout << purple  << endl;
   cout << peach   << endl;
   cout << darkRed << endl;

This usage reads naturally and the stream output generated is a readable description of the color. For example, outputting the color aqua results in:

           (red:0, green:50, blue:50)

Defining classes for the primary colors is staightforward. Because each of these colors has a common structure they can be defined via inheritance from the base class that is shown in the figure below.

The base class, PrimaryColor, defines a single data value representing the color's intensity value. In addition to the two constructors, a stream output operator is defined as a non-member friend function. The friend permission is used in the stream output operator to access the intensity data in the protected region of the PrimaryColor object. The first constructor of the PrimaryColor class can be viewed as a way to construct a PrimaryColor object using an int value or as a type conversion operator that converts an int value into a PrimaryColor object.



Base Class for the Primary Colors
class PrimaryColor
{  protected:
     int intensity;
   public:
     PrimaryColor(int i) : intensity(i) {}
     PrimaryColor(const PrimaryColor& c) { intensity = c.intensity; }
     friend ostream& operator<<(ostream& os, const PrimaryColor& pc);
};
ostream& operator<<(ostream& os, const PrimaryColor& pc)
{ os << pc.intensity;
  return os;
}

The individual classes Red, Green, and Blue can be derived from PrimaryColor; the next figure presents the Red class in detail; the Green and Blue classes are similar. Operator overloadings for the addition and subtraction operators are given for each of the Red, Green, and Blue classes. The overloadings allow the normal arithmetic operators to be used to add and substract colors in the same way that integer values are added and subtracted. For example, the code:

    Red lightRed(20);
     Red darkerRed(100);
     Red darkestRed = lightRed + darkerRed;
     Red lessRed = darkestRed - Red(30);

shows how Red objects can be declared and manipulated using these overloaded operators.


Definition of the Red, Green, and Blue Classes
class Red : public PrimaryColor
{
  public:
    Red(int i) : PrimaryColor(i) {}
    Red operator+(const Red& r) {return Red(intensity + r.intensity; }
    Red operator-(const Red& r) {return Red(intensity - r.intensity; }
};
class Green : public PrimaryColor
{
  // similar to Red class
};
class Blue : public PrimaryColor
{
  // similar to Red class
};

In addition to providing greater readability, the introduction of the simple classes for Red, Green, and Blue also helps to avoid certain errors that would occur were only basic int types used instead. For example, if a programmer unintentionally wrote the following code using only integer values:

    int color = 10; // meant to be red
     int shade = 20; // meant to be red
     int hue = 20;   // meant to be green
     ...
     int shade = color + hue;

the addition of a red integer and a green integer cannot be detected by a mistake by the compiler although it represents a programming mistake. The compiler is unable to help because the only type involved is the single type int. However, by introducing the Red, Green, and Blue classes this code becomes:

    Red   color(10);  // meant to be red
     Red   shade(20);  // meant to be red
     Green hue(20);    // meant to be green
     ...
     shade = color + hue;  // compile-time error

and the compiler is able to detect the unintended mixing of the two color values because of the extra types introduced by the Red, Green, and Blue classes.

The more general Color class can now be defined in terms of the three PrimaryColor classes. Each object of the Color class has encapsulated values that represent that color's combination of red, green, and blue intensities. Ignoring the possibility of type conversions, a first attempt at defining the operator overloading for the Color class is shown in the figure below.


Type Conversions for the Color Class
class Color 
{
   private:
       Red   red;
       Green green;
       Blue  blue;

   public:
      Color(Red r, Green g, Blue b) 
                  : red(r), green(g), blue(b) {} 

   friend ostream& operator<< (ostream& os, const Color& color);

   friend Color   operator+ (Red   r, Color c);
   friend Color   operator+ (Green g, Color c);
   friend Color   operator+ (Blue  v, Color c);
   friend Color   operator+ (Color c, Red   r);
   friend Color   operator+ (Color c, Green g);
   friend Color   operator+ (Color c, Blue  b);

   friend Color   operator- (Red   r, Color c);
   friend Color   operator- (Green g, Color c);
   friend Color   operator- (Blue  v, Color c);
   friend Color   operator- (Color c, Red   r);
   friend Color   operator- (Color c, Green g);
   friend Color   operator- (Color c, Blue  b);

};


These cases are necessary to account for all the combinations of primary colors, whether the Color is the right- or left-hand operand, and whether the operation is addition or subtraction.

A cleaner solution can be obtained by using type conversion to elevate a PrimaryColor object to a Color object before applying the overloaded addition or subtraction operator, which leads to the class defined in the following figure. In this class, the three constructors with a single argument play the role of type conversion operators because they produce an object of one class, a Color object, from an object of a different class, Red, Green, or Blue.



Defining the Color Class Using Type Conversions
class Color 
{
  private:
    Red   red;
    Green green;
    Blue  blue;

  public:
    Color(Red r, Green g, Blue b) 
                   : red(r), green(g), blue(b) {} 

    Color(const Red&   r) : red(r), green(0), blue(0) {}  //convert Red   to Color
    
    Color(const Green& g) : red(0), green(g), blue(0) {}  //convert Green to Color
    
    Color(const Blue&  b) : red(0), green(0), blue(b) {}  //convert Blue  to Color

    friend ostream& operator<< (ostream& os, const Color& color);

    friend Color   operator+ (const Color& c1, const Color& c2);
    friend Color   operator- (const Color& c1, const Color& c2);
};

   

The compiler utilizes the type conversion implicitly when it encounters an expression involving objects that are not of the same class. For example, in the code

    Color hue(Red(50), Green(50), Blue(50);
    Color shade = Red(10) + hue;

the expression "Red(10) + hue" involves an operation between an object of the Red class and an object of the Color class. No operator overloading is immediately recognized for this case. However, there is available the overloaded addition operator in the Color class that provides a way of adding together two objects of that class. Furthermore, there is a type conversion that defines how to convert the Red object to a Color object. Thus, the last statement above would be interpreted as if the programmer had written

    Color shade = Color(Red(10)) + hue;

For clarity, the programmer may prefer to write the code with the explicit type conversion operator. However, the compiler will supply the type conversion implicitly if such is required.

In summary, the use of type conversion operators, overloaded operators, and implicit type conversion by the compiler allows a compact set of operator overloadings to be written that provide for flexible ways of operating on the value of different classes.

  1. Include in the definition of the PrimaryColor class a type conversion operator that converts a PrimaryColor to an int. Rewrite the stream output operator so that it is no longer a friend function of the PrimaryColor class.

  2. Include in the definition of the PrimaryColor class a type conversion operator that converts a PrimaryColor to an int. Experiment with the effect this has on expressions that involve mixing objects of the Red, Green, and Blue classes, such as Red(10) + Green(5).

  3. Construct a different design for the color classes in which the Red, Green, and Blue classes define type conversion operators that convert objects of their classes to Color objects. For example, the type conversion applied to the object Red(10) would yield the object Color(Red(10), Green(0), Blue(0)). You will need to redefine parts of the Color class to make this design work. Are there differences between this design and the one above?

  4. Use the concepts of operator overloading and type conversions to define a Fraction class that could be used as follows:
    Fraction f(3,2); // represents 3/2 = 1.5
    					
    

    Fraction g(5,3); // represents 5/3

    ...

    Fraction h = f + 1;

    Fraction i = 1 + f;

    Fraction j = h - 1 + i;

 




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

Legal Statement

 

 

 

ÿ