7.2 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. These situations are: The first case is considered in this section and the second case is considered in the next section (Operator Overloading and Type Conversion).

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:

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

When operator overloading is achieved using non-member function there are two cases to be considered:

In the former case, the non-member function is written simply as a normal function. In the later 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] << " ]";

which prints our a comma separated list of numbers enclosed in brackets. This is tedious code to write each time and can be placed in the Array class itself as a "print" method as follows:
   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();
   };
   
where the Print method is implemented as:
   void Array::Print() { 
      cout << "[ ";
      for(int i = 0; i < 19; i++)
         cout << array[i] << ", ";
      cout << array[19] << " ]";
   }
This approach to displaying the array is can be improved upon by noting that the stream I/O operators ( "<<", ">>" ) are operators that can be overloaded. If such an overloading could be accomplished then it would be possible to write:
    Array a;
 
    // give values to a

    cout << a;

Overloading the stream I/O operators presents an apparent problem because it seems to require that the operator overloading must take place 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 following function could be defined:

     ostream& operator<< (ostream& os , Array& a) {
          os << "[ ";
          for(int i = 0; i < 19; i++)
              os << a[i] << ", ";
          os << a[19] << " ]";
     }
The class "ostream" used 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 << a;
where "a" is an object of the class Array, the compiler will check if there is an overloaded operator of the form "operator<<(Array)" in the ostream class (which it will not find) and then will check if there is 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 a;

	// give values to a

	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 for one of two reasons: 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.

An example of where special access is required is 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 form, not in ASCII (text form). This form of I/O is faster and is more compact. It is faster because one write operation is done to write the entire array. It is more compact because an integer, say 5,210,500, is stored in 4 bytes (32 bits) in the file in binary form but requires at least 8 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. However, 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 can be written that operate on files (ofstream, ifstream). For output, the following non-member functions is what 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. Special access is granted by a class declaring another function (or class) as its "friend". For the non-member function considered here, the Array class would be modified as follows:

   class Array {
    private:
       int array[20];
    public:
    ...
      friend ofstream& operator<< (ofstream& ofs, Array& a);
   ...
   };

The "friend" declaration grants the access to the private data that is required.

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

Last Updated: November 17, 1995 / kafura@cs.vt.edu