Main

Week 8

Operator overloading

Reference: TICPPv1, Chapter 12

Here is a very simple code that shows the essence of operator overloading: '+', '++ (post)', and '++ (pre)'.

  1. class A
  2. {
  3. public:
  4.     A(){}
  5.     explicit A(int n):data(n) {}
  6.     int data;
  7.     A operator+(A&);
  8.     A operator++(int);
  9.     A& operator++();
  10. };
  11.  
  12. // + overloading
  13. A A::operator+(A& obj)
  14. {
  15.     A tmp = *this;
  16.     tmp.data = this->data + obj.data;
  17.     return tmp;
  18. }
  19.  
  20. // post increment (x++) overloading
  21. // returns original value, and then increment the value
  22. // copy needed
  23. // return a locally created object.
  24. // Note that it's not returning a reference since it's a temporary obj.
  25. A A::operator++(int)
  26. {
  27.     A& tmp = *this;
  28.     this->data = this->data + 1;
  29.     return tmp;
  30. }
  31.  
  32. // pre increment (++x) overloading
  33. // returns incremented tvalue
  34. // no copy necessary
  35. A& A::operator++()
  36. {
  37.     this->data = ++(this->data);
  38.     return *this;
  39. }
  40.  
  41. int main()
  42. {
  43.     A obj1(10);
  44.     A obj2(20);
  45.     A obj3 = obj1 + obj2;  // obj3.data = 10 + 20 = 30
  46.     A obj4 = obj1++;       // obj4.data = 10, obj1.data = 11
  47.     A obj5 = ++obj2;       // obj5.data = 21, obj2.data = 21
  48.  
  49.     return 0;
  50. }

Operator overloading extends the function overloading concept to operators so that we can assign new meanings to C++ operators. It lets us extend operator overloading to user-defined types. That is by allowing us to use the "+" to add two objects. The compiler determines which definition of addition to use depending on the number and type of operands. Overloaded operators can often make code look more natural. In other words, operator overloading can be very useful to make our class look and behave more like built-in types.

To overload an operator, we use a special function, operator function. For example, when we overload "+":

 operator+(argument_list)

Suppose, for example, that we have a MyComplex class for which we define an operator+() member function to overload the + operator so that it adds one complex number to another complex number. Then, if c1, c2, c3 are all objects of MyComplex class, we can write this:

 c3 = c1 + c2;

The compiler, recognizing the operands as belonging to the MyComplex class, replaces the operator with the corresponding operator function:

 c3 = c1.operator+(c2);

The function then use the c1 object which invokes the method, and the c2 object is passed as an argument to calculate the sum, and returns it. Note that we use assignment operator = which is also need to be overload.

Operators that cannot be overloaded

The following operators cannot be overloaded:

  • . member selection
  • .* member selection with pointer-to-member
  • ?: conditional
  • :: scope resolution
  • # stringizing operator
  • ## merging operator
  • sizeof object size information
  • typeid object type information

Overloading '='

In this section we'll learn how to overload the assignment (=) operator between two objects of the MyComplex class.

Let's look at the following code:

  1. class MyComplex
  2. {
  3. private:
  4.     double real, imag;
  5. public:
  6.     MyComplex(){
  7.         real = 0;
  8.         imag = 0;
  9.     }
  10.  
  11.     MyComplex(double r, double i) {
  12.         real = r;
  13.         imag = i;
  14.     }
  15.  
  16.     double getReal() const {
  17.         return real;
  18.     }
  19.  
  20.     double getImag() const {
  21.         return imag;
  22.     }
  23.  
  24.     MyComplex & operator=(const MyComplex &);
  25. };
  26.  
  27. MyComplex & MyComplex::operator=(const MyComplex& c) {
  28.     real = c.real;
  29.     imag = c.imag;
  30.     return *this;
  31. }
  32.  
  33. #include <iostream>
  34.  
  35. int main()
  36. {
  37.     using namespace std;
  38.  
  39.     MyComplex c1(5,10);
  40.     MyComplex c2(50,100);
  41.     cout << "c1= " << c1.getReal() << "+" << c1.getImag() << "i" << endl;
  42.     cout << "c2= " << c2.getReal() << "+" << c2.getImag() << "i" << endl;
  43.     c2 = c1;
  44.     cout << "assign c1 to c2:" << endl;
  45.     cout << "c2= " << c2.getReal() << "+" << c2.getImag() << "i" << endl;
  46. }

We get the output as expected:

c1= 5+10i
c2= 50+100i
assign c1 to c2:
c2= 5+10i

Overloading '+'

In this section we'll learn how to overload the addition (+) operator between two objects of the MyComplex class.

Let's look at the following code which has '+' additional overloading function:

  1. class MyComplex
  2. {
  3. private:
  4.     double real, imag;
  5. public:
  6.     MyComplex(){
  7.         real = 0;
  8.         imag = 0;
  9.     }
  10.  
  11.     MyComplex(double r, double i) {
  12.         real = r;
  13.         imag = i;
  14.     }
  15.  
  16.     double getReal() const {
  17.         return real;
  18.     }
  19.  
  20.     double getImag() const {
  21.         return imag;
  22.     }
  23.  
  24.     MyComplex & operator=(const MyComplex &);
  25.     MyComplex & operator+(const MyComplex& );
  26.  
  27. };
  28.  
  29. MyComplex & MyComplex::operator=(const MyComplex& c) {
  30.     real = c.real;
  31.     imag = c.imag;
  32.     return *this;
  33. }
  34.  
  35. MyComplex & MyComplex::operator+(const MyComplex& c) {
  36.     real += c.real;
  37.     imag += c.imag;
  38.     return *this;
  39. }
  40.  
  41. #include <iostream>
  42.  
  43. int main()
  44. {
  45.     using namespace std;
  46.  
  47.     MyComplex c1(5,10);
  48.     MyComplex c2(50,100);
  49.     cout << "c1= " << c1.getReal() << "+" << c1.getImag() << "i" << endl;
  50.     cout << "c2= " << c2.getReal() << "+" << c2.getImag() << "i" << endl;
  51.     c2 = c1;
  52.     cout << "assign c1 to c2:" << endl;
  53.     cout << "c2= " << c2.getReal() << "+" << c2.getImag() << "i" << endl;
  54.     cout << endl;
  55.  
  56.     MyComplex c3(10,100);
  57.     MyComplex c4(20,200);
  58.     cout << "c3= " << c3.getReal() << "+" << c3.getImag() << "i" << endl;
  59.     cout << "c4= " << c4.getReal() << "+" << c4.getImag() << "i" << endl;
  60.     MyComplex c5 = c3 + c4;
  61.     cout << "adding c3 and c4" << endl;
  62.     cout << "c3= " << c3.getReal() << "+" << c3.getImag() << "i" << endl;
  63.     cout << "c4= " << c4.getReal() << "+" << c4.getImag() << "i" << endl;
  64.     cout << "c5= " << c5.getReal() << "+" << c5.getImag() << "i" << endl;
  65. }

Note that when we're using '+' for the object of MyComplex type,

 c5 = c3 + c4;

actually, we are calling a function like this:

 c5 = c3.operator+(c4)

The output of the code above is:

c1= 5+10i
c2= 50+100i
assign c1 to c2:
c2= 5+10i

c3= 10+100i
c4= 20+200i
adding c3 and c4
c3= 30+300i
c4= 20+200i
c5= 30+300i

We got the right result at least for c5. But the value of c3 has been changed.

What happened?

Let look at the code overloading '+'.

  1. MyComplex & MyComplex::operator+(const MyComplex& c) {
  2.     real += c.real;
  3.     imag += c.imag;
  4.     return *this;
  5. }

As it turned out, the operation inside the function returning the reference to c3 object which has been changed.

So, let's rewrite the overloading function.

  1. const MyComplex operator+(const MyComplex & );
  2.  
  3. const MyComplex MyComplex::operator+(const MyComplex& c) {
  4.     MyComplex temp;
  5.     temp.real = this->real + c.real;
  6.     temp.imag = this->imag + c.imag;
  7.     return temp;
  8. }

Note that this doesn't return MyComplex &, but instead returns a const MyComplex class variable. As you can see, the implementation is a little bit different from the previous example. Here, we're not returning *this. Instead, we're creating a temporary variable and assigning the results of the addition to temp. This explains why the function returns const MyComplex and not MyComplex &. In other words, the function creates a new MyComplex object temp that represents the sum of the other two MyComplex objects. Returning the object creates a copy of the object that the calling function can use. If the return type were MyComplex &, however, the reference would be the temp object. But the temp object is a local variable and is destroyed when the function returns, so the reference would be a reference to a nonexisting object. Using a MyComplex return type, however, means the program constructs a copy of MyComplex object before destroying it, and the calling function gets the copy.

Then, we'll get the right answer.

c1= 5+10i
c2= 50+100i
assign c1 to c2:
c2= 5+10i

c3= 10+100i
c4= 20+200i
adding c3 and c4
c3= 10+100i
c4= 20+200i
c5= 30+300i

Overloading the ostream (<<) operator and the istream (>>) operator

Output streams use the << operator for standard types. We can also overload the << operator for our own classes.

Actually, the << is left shift bit manipulation operator. But the ostream class overloads the operator, converting it into an output tool. The cout is an ostream object and that it is smart enough to recognize all the basic C++ types. That's because the ostream class declaration includes an overloaded operator<<() definition for each of the basic types.

The istream operator can be overloaded almost the same way except the 2nd parameter is not const.

  1. #include <iostream>
  2. using namespace std;
  3.  
  4. class MyComplex
  5. {
  6. private:
  7.     double real, imag;
  8. public:
  9.     MyComplex(){
  10.         real = 0;
  11.         imag = 0;
  12.     }
  13.  
  14.     MyComplex(double r, double i) {
  15.         real = r;
  16.         imag = i;
  17.     }
  18.  
  19.     double getReal() const {
  20.         return real;
  21.     }
  22.  
  23.     double getImag() const {
  24.         return imag;
  25.     }
  26.  
  27.     MyComplex & operator=(const MyComplex &);
  28.     const MyComplex operator+(const MyComplex & );
  29.     MyComplex & operator++(void);
  30.     MyComplex  operator++(int);
  31.  
  32.     /*friend const
  33.         MyComplex operator+(const MyComplex&, const MyComplex&); */
  34.  
  35.     friend ostream& operator<<(ostream& os, const MyComplex& c);
  36.  
  37.         // note: no const for the second parameter
  38.         friend istream& operator>>(istream& is, MyComplex& c);
  39. };
  40.  
  41. MyComplex & MyComplex::operator=(const MyComplex& c) {
  42.     real = c.real;
  43.     imag = c.imag;
  44.     return *this;
  45. }
  46.  
  47. const MyComplex MyComplex::operator+(const MyComplex& c) {
  48.     MyComplex temp;
  49.     temp.real = this->real + c.real;
  50.     temp.imag = this->imag + c.imag;
  51.     return temp;
  52. }
  53.  
  54. //pre-increment
  55. MyComplex & MyComplex::operator++() {
  56.     real++;
  57.     imag++;
  58.     return *this;
  59. }
  60.  
  61. //post-increment
  62. MyComplex MyComplex::operator++(int) {
  63.         MyComplex temp = *this;
  64.         real++;
  65.         imag++;
  66.         return temp;
  67. }
  68.  
  69. /* This is not a member function of MyComplex class */
  70. /*
  71. const MyComplex operator+(const MyComplex& c1, const MyComplex& c2) {
  72.         MyComplex temp;
  73.         temp.real = c1.real + c2.real;
  74.         temp.imag = c1.imag + c2.imag;
  75.         return temp;
  76. }*/
  77.  
  78.  
  79. ostream& operator<<(ostream &os, const MyComplex& c) {
  80.     os << c.real << '+' << c.imag << 'i' << endl;
  81.     return os;
  82. }
  83.  
  84.  
  85. istream& operator>>(istream &is, MyComplex& c) {
  86.     is >> c.real >> c.imag;
  87.     return is;
  88. }
  89.  
  90. int main()
  91. {
  92.     MyComplex c1(5,10);
  93.     cout << "c1 = " << c1.getReal() << "+" << c1.getImag() << "i" << endl;
  94.  
  95.     cout << "Using overloaded ostream(<<) " << endl;
  96.     cout << "c1 = " << c1 << endl;
  97.  
  98.     MyComplex c2;
  99.     cout << "Enter two numbers: " << endl;
  100.     cin >> c2;
  101.     cout << "Using overloaded istream(>>) " << endl;
  102.     cout << "Input complex is = " << c2;
  103.  
  104.     return 0;
  105. }

The output is:

c1 = 5+10i
Using overloaded ostream(<<)
c1 = 5+10i

Enter two numbers:
111 222
Using overloaded istream(>>)
Input complex is = 111+222i
Note that we just used:

cout << "c1 = " << c1 << endl;
Note that when we do

cout << c1;
it becomes the following function call:

operator<<(cout, c1);

Return types for overloaded operators

Returning a const object

The MyComplex::operator+() in the example has a strange property. The intended use is this:

 MyComplex c6 = c1 + c2;	// #1

But the definition also allows us to use the following:

 MyComplex c7;
 c1 + c2 = c7;	// #2

This code is possible because the copy constructor constructs a temporary object to represent the return value. So, in the code, the expression c1 + c2 stands for that temporary object. In statement #1, the temporary object is assigned to c6. In statement #2, c7 is assigned to the temporary object.

The temporary object is used and then discarded. For instance, in statement #2, the program computes the sum of c1 and c2, copies the answer into the temporary return object, overwrites the contents with the contents of c7, and then discards the temporary object. The original complex numbers are all left unchanged.

If we declare the return type as a const object, we can avoid this problem and disallow the #2 case.

const MyComplex  operator+(const MyComplex & c) const {
	return MyComplex(real + c.real, imag + c.imag);
}

Why const with a reference return?

Let's look at the following example:

  1. #include <iostream>
  2.  
  3. struct node
  4. {
  5.     int data;
  6. };
  7.  
  8. const node & makeNode(node &cref);
  9.  
  10. int main()
  11. {
  12.     node nodeA = {0};
  13.     std::cout << "1: nodeA.data = " << nodeA.data << std::endl;
  14.     makeNode(nodeA);
  15.     std::cout << "2: nodeA.data = " << nodeA.data << std::endl;
  16.     node nodeB;
  17.     nodeB = makeNode(nodeA);
  18.     std::cout << "3: nodeA.data = " << nodeA.data << std::endl;
  19.     std::cout << "1: nodeB.data = " << nodeB.data << std::endl;
  20.  
  21.     node nodeC;
  22.     // makeNode(nodeB).data = 99;
  23.  
  24.     return 0;
  25. }
  26.  
  27. const node & makeNode(const node &ref)
  28. {
  29.     std::cout << "call makeNode()\n";
  30.     ref.data++;
  31.     const return ref;
  32. }
  33.  

The makeNode() function return type is const node &. What's the purpose of const? It does not mean that the node structure itself is const; it just means that we can't use the return value directly to modify the structure.

If we omitted const, we could use the code below:

 makeNode(nodeB).data = 99;

Because makeNode returns a reference to nodeB, this code is the same as this:

 makeNode(NodeB);
 nodeB.data = 99;

But because of the const, we can't use the return value directly to modify the structure, and with the const, the code:

 makeNode(nodeB).data = 99;

won't compile.

Summary

  • If a method or function returns a local object, it should return an object, not a reference.
  • If a method or function returns an object of a class for which there is no public copy constructor, such as ostream class, it must return a reference to an object.
  • Some methods and functions, such as the overloaded assignment operator, can return either an object or a reference to an object. The reference is preferred for reasons of efficiency.
  • It is an error to return a pointer to a local object. Once the function completes, the local objects are freed. The pointer would be a dangling pointer that refers to a nonexistent object.

Green Marinee theme adapted by David Gilbert, powered by PmWiki