8.3 Operator Overloading and Non-Member Functions



There are two situations under which operator overloading must be done by functions that are not members of a specific class. The first is when the class to which the member function should be added is not available for modification. This frequently occurs with classes that are in standard class libraries; an example is the stream input-output (I/O) library. The second instance is when type conversion of the arguments involved in the operation is desired. The first case is considered in this section and the second in the next section.

To allow operator overloading by non-member functions, the rules used by the compiler involve two steps. If an expression of the form "x op y" is encountered, the compiler will check:

  • is there a member function in the class of object x of the form "operator op(Y)" where Y is the class of object y, and, if not,
  • is there a non-member function of the form "operator op(X,Y)" where X is the class of object x and Y is the class of object y.

The rules are applied in this order. An overloaded operator will be used if either of them is satisfied.

When operator overloading is achieved using non-member function, there are two cases to be considered: the overloaded operator uses only the public interface of the class(es) involved in the overloading, or it requires access to the private data of the class(es). If the former, the non-member function is written simply as a normal function. If the latter case, a special "friend" designation is used to grant access to private data, access which otherwise would be denied because the function is not a member of the class whose private data is being accessed.


Non-Member Functions Without Special Access


Overloading of the stream I/O operators for the Array class illustrates the use of non-member function to overload an operator when the class in which the operator is defined is unavailable for modification. Without operator overloading, the usual way to print the contents of an Array object is as follows:

       Array a;

        // give values to a

        cout << "[ ";
        for(int i = 0; i< 19; i++)
           cout << a[i] << ", ";
        cout << a[19] << " ]";

This prints out a comma-separated list of numbers enclosed in brackets, and is tedious code to write each time. It can, however, be placed in the Array class as a print method. The following figure shows the definition of the Array class and the implementation of the print method.


Input-Output (I/O) Using a Standard Method of a Class
class Array
{
    private:
      int array[20];
    public:
             Array();
      int&   operator[](int i);         // subscript   operator
      Array& operator+(Array& other);   // addition    operator
      Array& operator-(Array& other);   // subtraction operator
      void   Print();
            ~Array();
};
// in implementation
   
void Array::Print() 
{ 
      cout << "[ ";
      for(int i = 0; i < 19; i++)
         cout << array[i] << ", ";
      cout << array[19] << " ]";
}

  

This approach to displaying the array may be improved upon by noting that the stream I/O operators ( <<, >> ) can be overloaded. If such an overloading could be accomplished, then it would be possible to write:

   Array array;
 
    // give values to array

    cout << array;

But, overloading the stream I/O operators presents an apparent problem because it seems to require that the operator overloading occur in the library class that implements stream I/O. Changing the library classes may be impossible, because the source code is not available, or at least troublesome, due to the need to understand the classes that implement the stream I/O functionality.

C++ allows operators to be overloaded by functions that are not members of a class. To provide the overloading of the stream I/O operators, the I/O functionality could be implemented as follows:


Defining a Stream I/O Function
ostream& operator<< (ostream& os , Array& a) 
{
    os << "[ ";
    for(int i = 0; i < 19; i++)
        os << a[i] << ", ";
    os << a[19] << " ]";
}

    

The class ostream in this example is the actual type of the predefined library variable cout (similarly cin is implemented by the class istream). Upon examining the statement

    cout << array;

where array is an object of the class Array, the compiler will check to see if there is an overloaded operator of the form "operator<<(Array)" in the ostream class (which it will not find) and then will check for a non-member overloaded operator of the form "operator<<(ostream&, Array)" (which it will find).

Since the overloaded stream output operator is defined in terms of an ostream object, it applies not only to cout but to other objects that inherit from the ostream class. The ofstream class, for file output, inherits from the ostream class. Thus, the overloading above allows

       ofstream outFile("array.data");

        Array array;

        // give values to array

        outFile << a;

Similarly, writing to "string streams" is also possible.


Non-Member Function with Friend Access


Non-member functions may be given special access to the private or protected data of a class in instances where they need information about the class that is not accessible through the public interface of the class, or
efficiency considerations make it necessary for the non-member function to bypass the public interface and be given direct access to the class's encapsulated data.

It should be stressed that granting special access to private data should be used sparingly. Unnecessary use of this feature undermines the value of encapsulated data and weakens the structure of the system.

Special access is required, for example, when efficient binary level I/O is added to the Array class given above. What is desired is a way to write the contents of the Array into a file in binary, not ASCII (text ) form. This approach to I/O is faster and is more compact: it is faster because one write operation is done to write the entire array, and more compact because an integer, say 5,210,500, is stored in four bytes (32 bits) in the binary form file but requires at least eight byes in ASCII. However, to perform the binary-level I/O operation, it is necessary to know the address of the array to be written, but it is undesireable to add a method in the Array class to return the base address of its encapsulated array, as this would allow any code to manipulate the encapsulated array. A better design in this situation is to allow the non-member function that implements the stream I/O to have special access. This allows the overloading function, and only the overloading function, to have access to the base address of the encapsulated array.

The binary form of stream I/O should look and behave as the other forms of stream I/O overloading. Thus, binary form of the stream I/O should be usable as follows:

      ofstream binaryFile("array.bin");  
        
       Array a;

       //... give values to a

       binaryFile << a;                 // output array in binary form
       binaryFile.close();

To make this work, an overloading of the stream I/O operators may be written that operates on files (ofstream, ifstream). For output, the following non-member functions is needed:

  ofstream& operator<< (ofstream& ofs, Array& a) 
   {
      ofs.write((char*)(a.array), 20*sizeof(int));
      return ofs;
   };

As can be seen, this non-member function requires access to the private data of the Array class. To allow this access to be granted, it is necessary to give the non-member function special permission. This is accomplished via a class declaring another function (or class) as its "friend." For the non-member function considered here, the Array class would be modified as shown in the figure below. The friend declaration grants the access to the private data that is required.


Declaring a "friend" Stream I/O Operator
class Array {
    private:
       int array[20];
    public:
    ...
      friend ofstream& operator<< (ofstream& ofs, Array& a);
   ...
   };

  

Notice also that two overloadings of the stream I/O operators have been defined - one for ostream& objects (like cout) and one for ofstream& objects (like the binaryFile above). These overloadings perform ASCII output for streams that are typically viewed by users and binary I/O on files.

 

  1. Implement and test an overloaded stream output operator for the Number class that does not use friend access.

  2. Implement and test an overloaded stream output operator for the Message class that does not use friend access.

  3. Implement and test an overloaded stream output operator for the Number class that uses friend access.

  4. Implement and test an overloaded stream output operator for the Message class that uses friend access.

  5. Implement and test an overloaded stream output operator for the Association class, creating an association between pairs of ints defined in the exercises accompanying section 8.2.

  6. Implement and test an overloaded stream output operator for the Association class, creating an association between a char* and an int defined in the exercises accompanying section 8.2.

 




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

Legal Statement