Operator Overloading


Operator Overloading

oop



Learning Objectives

  • Define overloaded operator functions in C++
  • Make an appropriate decision about whether an overloaded operator function should be global or a member function
  • Make good use of friends in C++
  • Explain in what situations the assignment operator should be overloaded in C++, and make good use of the ‘this’ pointer when implementing an overloaded assignment operator

Definition of Overloading

Overloading is where multiple functions are defined with the same name but have different prototypes.

There are some pre-defined operators for common basic data types. For example the simple arithmetic operators (+, -, etc.) are defined for the int,float and char data types. Multiple ‘overloaded’ implementations of these basic operators is known as operator overloading. These basic operators are in many ways similar to functions as they have names, take arguments and return values. However, they way in which they are applied differs. If a C++ function called plus is written to perform addition, it would be used as follows:

x = plus (y, 2);

However, using the built-in operator + the same operation can be performed as follows:

x = y + 2;

In other words, the operator symbol + appears between two arguments, instead of before them. The + is a binary infix operator, as it takes two arguments and the operator appears in between them. The + - * / operators are all binary infix operators. The ++ and -- operators are unary operators as they take a single argument. They can be used either as prefix (before the argument) or postfix (after the argument) operators.

These built-in operators in can be overloaded but they are only defined for use with basic data types, i.e int, char, bool, float, long int, short and double. If a new class is defined then it will not be possible to use these operators with instances of the new class.

For example, if a new class Rational is created to store information about and perform calculations with rational numbers (i.e. numbers that can be written as ratio of two integers, e.g. 3/7). Now with this new class it would be convenient if the built-in arithmetic operators could be used to perform calculations, as in the following:

Rational r1 (2, 5); //define rational number 2/5 2 
Rational r2 (5, 7); //define rational number 5/7 3 
Rational r3;
r3 = r1 * r2; // use built-in multiply operator

However as the built-in operators are only defined for the basic data types, this isn’t possible. Operator overloading is the C++ mechanism allowing the use of built-in operators for classes that have been created from scratch.

Operator Overloading Implementation

To illustrate the use of operator overloading in C++, lets consider the following scenario.

Real, rational and complex numbers are all types of number in the domain of numbers. All numbers can be negated but precisely what negation means is dependent on the type of number. Similarly, it should be possible to display all numbers but exactly how they are displayed will vary. It should not be possible to create an instance of a number, only real, rational or complex numbers. A real number is represented by a single floating point value. A complex number comprises two real numbers, one for real (i.e. non-imaginary) and one for the imaginary part. You should be able to compute the conjugate of a complex number (i.e just negate the imaginary part). A rational number consists of two integers, one for the numerator and one for the denominator.

First lets draw a UML class diagram of this scenario, using the OO design steps mentioned in the last chapter:

UML DIAGRAM

Using the UML class the following header file for rational numbers can be written:

Rational.h
class Rational : public Number 
{
  public:
    Rational () {}
    int GetN() const { return _n; } 
    void SetN(int val) { _n = val; } 
    int GetD() const { return _d; }
    void SetD(int val) { _d = val; }
    void Negate () { _n = -n; }
    void Display () const
    {cout << _n << " / " << _d;}
  protected:
    int _n; 
    int _d;
};

In order to be able to use the basic arithmetic operators such as * with instances of the Rational class we must overload them so that they can take Rational instances as arguments. The following code, which can be added to the rational.h file, achieves this aim.

Rational operator* (const Rational& r1, const Rational& r2)
{
   Rational ret; 
   int n = r1.GetN()*r2.GetN();
   int d = r1.GetD()*r2.GetD();
   ret.SetN(n);
   ret.SetD(d);
   return ret;
}

See how the new overloaded operator function is not a member function of Rational (although normally it can be as we’ll see later). The name of an operator function is the word operator followed by the symbol for the operator we want to overload, e.g. operator+ for the addition operator, operator[] for the array subscript operator, etc.

The overloaded operator function returns a single value of type Rational. Recall that the * operator is a binary operator: therefore, the over-loaded operator* function takes two arguments, both of type Rational. Since the arguments should not be modified as a result of the operation, both are specified as const arguments.

Reference Types

In the previous example both arguments to operator* were defined as pass-by-reference parameters i.e. using the & symbol. When appended to a type (e.g. Rational&), the & symbol creates a new type called “a reference to” the type. A reference type is like a pointer except that it doesn’t need to be dereferenced to access the value pointed to. Also, when a reference type is const it means that we can’t change the object itself (for a const pointer it’s just the pointer that can’t be changed).

In Rational.h we could have defined both arguments as being of type Rational rather than const Rational&. A const reference type was used purely for efficiency reasons but this is very common when overloading operators. If the arguments had a Rational type, then copies of the Rational instances would be created each time the * operator was used; when using a const reference type only the reference is passed but still the value pointed to cannot change.

Friends

In the earlier example, the overloaded operator function had to use the inspector functions provided by the Rational class (i.e. GetN() and GetD()) to access its data members. In this simple example, this is ok but for more complex classes this may become cumbersome. Also, because the overloaded operator function is specific to the Rational class, it seems natural that it should have access to private and protected members. As the implementation stands this is not possible, however C++ provides a mechanism for such access: the friend keyword. Consider the following modified implementation of the Rational class:

rational.h
class Rational : public Number 
{
  public:
    Rational () {}
    int GetN() const { return _n; } 
    void SetN(int val) { _n = val; } 
    int GetD() const { return _d; }
    void SetD(int val) { _d = val; }
    void Negate () { _n = -n; }
    void Display () const
    {cout << _n << " / " << _d;}
    friend Rational operator* (const Rational& r1,
                               const Rational& r2);
  protected:
    int _n; 
    int _d;
};

Rational operator* (const Rational& r1, const Rational& r2)
{
   Rational ret; 
   int n = r1._n*r2._n;
   int d = r1._d*r2._d;
   ret.SetN(n);
   ret.SetD(d);
   return ret;
}

By adding the prototype of the operator* function inside the Rational class definition, preceded by the friend keyword, gives the operator* function all of the access privileges that come with being a class member. Therefore when accessing the data members of Rational (e.g. int n = r1._n*r2._n;), they can be referred to directly rather than using the inspector and mutator functions.

Note: in the case of the * operator it would have been possible to implement the operator overloading by making the operator* function a member function. More on this later.

Overloading the Output Operator

Another example of operator overloading is overloading the output operator (i.e. <<). Consider the following addition to the previous example code:

rational.h addition
ostream& operator<< (ostream& os, const Rational& r)
{
  os << r._n << "/" << r._d;
  return os;
} 

Note that because operator<< accesses the data members of Rational directly it will need to be made a friend of Rational

The overloaded << operator function takes two arguments: an stream reference and an instance of the class (which is a const reference again for efficiency reasons). ostream is actually a type of out: recall that << is a binary infix operator and when used to send data to standard output its left-handed argument should be cout; the right-hand argument should be the data sent. The ostream reference argument can be used just like count in the function body of operator<<. Now the Rational class can be used as follows:

Rational r;
...
cout << r;

For the use of <<, the input arguments are cout (ostream instance) and r (Rational instance). The return type of the operator<< is another ostream instance which means it is possible to chain multiple << operators in the same statement, e.g.

cout << "Number: " << r << endl;

Note that ofstream is derived from the stream, so the overloaded << can also be used for output to external files.

Overloading the Assignment Operator

The assignment operator is already defined for classes that are defined by the programmer. For example, it is possible to use the assignment operator on instances of the Rational class as defined above. The result of this built-in assignment operator is to perform member-by-member assignment of all data members of the class. Therefore, so ling as the assignment operation is defined for all of these data members we normally don’t need to overload the assignment operator ourselves.

Such member-by-member assignment is sometimes not appropriate for classes that have one or more pointers as data members. As the values pointed to will not change, only the pointers themselves. If this is not the desired behaviour, then the assignment operator must be overloaded to implement the required behaviour. Let’s look at the Rational class once more:

Rational.h
class Rational {
...
  public:
    Rational& operator= (const Rational &r)
    {
      // do the copy
      _d = r._d;
      _n = r._n;
      
      // return the existing object
      return *this;
      }
...
};

The overloaded assignment operator Rational& operator= (const Rational &r) { ... } is a member function of the Rational class. Note that the assignment operator is a binary operator (i.e. it takes two arguments), and that it has to change the value of one of these arguments (the one on the left-hand side of the assignment).

However, although assignment is a binary operator the operator= function only takes a single argument. This is because, when an overloaded operator function is a member function of the class, the left-hand argument is always the class instance itself. Therefore, the number of arguments to the overloaded operator function should be reduced by one. The single argument of the operator= function represents the right-hand side of the assignment operation, in this case an instance of the Rational class (a const reference again).

The This Pointer

In the last example, the function body returned a special variable called this, which is a built in C++ pointer variable which is included in every class, and points to the current instance of the the class. In most cases we don’t need to use it as if we’re in a member function of a class and we refer to another member (i.e data member or member function) as it automatically refers to the one in the current instance.

However, when overloading the assignment operator we need to return the current instance as the return value of the overloaded operator function (this is a pointer so it needs dereferencing using the * symbol). Returning the current instance is required because an assignment operation does return a value; this is what makes it possible to chain assignments, e.g. x = y = 10. Therefore, the this pointer is needed to access the current instance.

Assignment Operator vs Copy Constructor

A copy constructor is a class constructor that takes another class as its argument. This is similar to the use of overloaded assignment operators, but there is a key difference. Below, the code illustrates this:

Rational r1, r2, r3;
...
Rational r1 = r4; // copy constructor used
r3 = r1;          // overloaded assignment used

The copy constructor is used when an instance is created and assigned to at the same time. The overloaded assignment operator is used when an existing instance is assigned to.

Note that both the copy constructor and the overloaded assignment operator are useful in the same situation, i.e. when a “deep copy” (i.e. copy the objects pointed to rather than the pointers themselves) is performed of pointer data members

Overloading a Unary Operator

All the operator overloading examples, we have seen so far have overloaded binary operators, i.e. those that take two arguments. For instance, for the * operator the overloaded function took two Rational arguments, and for the << operator it took a ostream and a Rational argument. The code outline below shows how to overload the ++ operator:

class Rational {
...
  public:
    Rational& operator++() // prefix
    {
      _n += _d;
      return *this;
    }
...
};

The overloaded operator function is a member function of the class, as it makes changes to its data members. The function takes no arguments as it just changes the current instance and returns the same instance with the this pointer.

List of Overloadable Operators

The following table summarises all of the operators that can be overloaded in C++, together with their normal (i.e. non-overloaded) meanings.

Operator Normal Meaning
+ Addition
_ Subtraction
* Multiplication
/ Division
++ Increment
-- Decrement
= Assignment
() Function Cell
[] Array Subscript
-> Indirect member
% Modulus
|| Logical OR
| Bitwise OR
&& Logical AND
! Logical NOT
!= Not Equal to
> Greater than
>= Greater than or equal to
< Less than
<= Less than or equal to
== Equal to
+= Add to
-= Subtract from
*= Multiply by
/= Divide by

Global or Member Functions

Most operators can be either global or member functions of a class. However, one rule is that if it is a member function, then the left-handed operand of the operator must be an instance of the class. For example, in the code shown below r1 is the left-hand operand and r2 is the right-hand operand.

Rational r1, r2;
...
r1 = r2

As the left-hand operand (i.e. r1) is Rational, it is permitted for the overloaded assignment operator to be a member function. Similarly, the overloaded * operator in the previous example could have been a member function because its left-hand operand was also Rational.

However if the left-hand operand is not of the class type (such as operator<< in which the left-hand operand is ostream&), the operator must be overloaded as a global function. In this case it should be a friend of the class if it needs access to private or protected members.

A second rule is that the assignment (=), subscript ([]), call (()), and member selection (->) operators must be overloaded as member functions. Although most operators can be overloaded as either global or member functions, it is typical (and good practice) to make an overloaded operator function a member function if it changes the data members of the class. Otherwise it should be global.

Overloaded Operator Function Prototype

Once we have decided whether to make the overloaded operator function global or a member function, the next stage is to form its prototype (i.e. decide how many arguments it should have and of which type, and which return type it should have). The following are useful guidelines to forming the overloaded function’s prototype:

Member Functions

If it’s a member function, the left-hand operand becomes the current instance (i.e. *this). All other operands become function arguments. Therefore, we should define one fewer argument to the overloaded operator function, e.g. if its a binary operator, we should define one argument representing the right hand operand. And if it’s a unary operator, we should define no arguments at all.

Operator Function Prototype
= Type& operator= (const Type&)
+= Type& operator+= (const Type&)
-= Type& operator-= (const Type&)
*= Type& operator*= (const Type&)
/= Type& operator/= (const Type&)
++ (prefix) Type& operator++ ()
-- (prefix) Type& operator-- ()
[] Type& operator[] (int)
Global Functions

If its a global function, for binary operators we should define two arguments to the overloaded operator function (as in the * example). If it’s a unary operator we should define one argument.

Operator Function Prototype
+ Type operator+ (const Type&, const Type&)
- Type operator- (const Type&, const Type&)
* Type operator* (const Type&, const Type&)
/ Type operator/ (const Type&, const Type&)
<< ostream operator<< (ostream&, const Type&)
>> istream operator>> (istream&, Type&)
== int operator== (const Type&, const Type&)
> int operator> (const Type&, const Type&)
>= int operator>= (const Type&, const Type&)
< int operator< (const Type&, const Type&)
<= int operator<= (const Type&, const Type&)
! Type operator! (const Type& b)

String Library Revisited

The file is not part of C++ functionality it is just a predefined class and it uses operator overloading to make it easy to use. For example consider the following uses of `string` objects:

string s;
s = "hello";             // overloading assignment operator
cout << s << endl;       // overloading << operator
string s2 = s + "world"; // overloading + operator
char c = s[2];           // overloading [] operator

Each of the above statements use an overloaded operator function that has already been defined in the string library. This is a good example of illustrating the point that using operator overloading can be seen as part of information hiding.


return  link
Written by Tobias Whetton