Operator Overloading

C++ has a number of built-in types, including int, real, char, and so forth. Each of these has a number of built-in operators, such as addition (+) and multiplication (*). C++ enables you to add these operators to your own classes as well.

In order to explore operator overloading fully, Listing 14.1 creates a new class, Counter. A Counter object will be used in counting (surprise!) in loops and other applications where a number must be incremented, decremented, or otherwise tracked.

Listing 14.1. The Counter Class
 0:  // Listing 14.1
 1:  // The Counter class
 2:  #include <iostream>
 3:
 4:  class Counter
 5:  {
 6:  public:
 7:      Counter();
 8:      ~Counter(){}
 9:      int GetItsVal()const { return itsVal; }
10:      void SetItsVal(int x) {itsVal = x; }
11:
12:  private:
13:      int itsVal;
14:  };
15:
16:  Counter::Counter():
17:  itsVal(0)
18:  {}
19:
20:  int main()
21:  {
22:      Counter i;
23:      std::cout << "The value of i is " << i.GetItsVal()
24:          << std::endl;
25:      return 0;
26:  }


The value of i is 0.
					

As it stands, this is a pretty useless class. It is defined on lines 4–14. Its only member variable is an int. The default constructor, which is declared on line 7 and whose implementation is on line 18, initializes the one member variable, itsVal, to 0.


Unlike a real, built-in, honest, red-blooded int, the counter object cannot be incremented, decremented, added, assigned, or otherwise manipulated. In exchange for this, it makes printing its value far more difficult!

Writing an Increment Function

Operator overloading provides much of the functionality that would otherwise be missing in user-defined classes such as Counter. When you implement an operator for your class, you are said to be “overloading” that operator. Listing 14.2 illustrates how to overload the increment operator.

Listing 14.2. Overloading the Increment Operator
 0:  // Listing 14.2
 1:  // Overloading the increment operator
 2:  #include <iostream>
 3:
 4:  class Counter
 5:  {
 6:  public:
 7:      Counter();
 8:      ~Counter(){}
 9:      int GetItsVal()const { return itsVal; }
10:      void SetItsVal(int x) {itsVal = x; }
11:      void Increment() { ++itsVal; }
12:      const Counter& operator++ ();
13:
14:  private:
15:      int itsVal;
16:  };
17:
18:  Counter::Counter():
19:  itsVal(0)
20:  {}
21:
22:  const Counter& Counter::operator++()
23:  {
24:      ++itsVal;
25:      return *this;
26:  }
27:
28:  int main()
29:  {
30:      Counter i;
31:      std::cout << "The value of i is " << i.GetItsVal()
32:          << std::endl;
33:      i.Increment();
34:      std::cout << "The value of i is " << i.GetItsVal()
35:          << std::endl;
36:      ++i;
37:      std::cout << "The value of i is " << i.GetItsVal()
38:          << std::endl;
39:      Counter a = ++i;
40:      std::cout << "The value of a: " << a.GetItsVal();
41:      std::cout << " and i: " << i.GetItsVal() << std::endl;
42:      return 0;
43:  }


The value of i is 0
The value of i is 1
The value of i is 2
The value of a: 3 and i: 3
						

On line 36 you can see that the increment operator is invoked


++i;

This is interpreted by the compiler as a call to the implementation of operator++ shown on lines 22–26, which increments its member variable itsValue and then dereferences the this pointer to return the current object. This provides a Counter object to be assigned to a. If the Counter object allocated memory, it would be important to override the copy constructor. In this case, the default copy constructor works fine.

Note that the value returned is a Counter reference, thereby avoiding the creation of an extra temporary object. It is a const reference because the value should not be changed by the function using this Counter.

Overloading the Postfix Operator

What if you want to overload the postfix increment operator? Here the compiler has a problem. How is it to differentiate between prefix and postfix? By convention, an integer variable is supplied as a parameter to the operator declaration. The parameter's value is ignored; it is just a signal that this is the postfix operator.

The Difference Between Prefix and Postfix

Before you can write the postfix operator, you must understand how it is different from the prefix operator. To review, prefix says increment and then fetch while postfix says fetch and then increment.

Therefore, while the prefix operator can simply increment the value and then return the object itself, the postfix must return the value that existed before it was incremented. To do this, a temporary object must be created. This temporary object will hold the original value while the value of the original object is incremented. The temporary object is returned, however, because the postfix operator asks for the original value, not the incremented value.

Let's go over that again. If you write

a = x++;

Then x is 5, after this statement a is 5, but x is 6. The value in x is returned and assigned to a, which increases the value of x. If x is an object, its postfix increment operator must stash away the original value (5) in a temporary object, increment x's value to 6, and then return that temporary object to assign its value to a.

Note that because the temporary object is being returned, it must be returned by value and not by reference, because it will go out of scope as soon as the function returns.

Listing 14.3 demonstrates the use of both the prefix and the postfix operators.

Listing 14.3. Prefix and Postfix Operators
 0:  // Listing 14.3
 1:  // Returning the dereferenced this pointer
 2:  #include <iostream>
 3:
 4:  class Counter
 5:  {
 6:  public:
 7:      Counter();
 8:      ~Counter(){}
 9:      int GetItsVal()const { return itsVal;}
10:      void SetItsVal(int x) {itsVal = x; }
11:      const Counter& operator++ ();      // prefix
12:      const Counter operator++ (int); // postfix
13:
14:  private:
15:      int itsVal;
16:  };
17:
18:  Counter::Counter():
19:  itsVal(0)
20:  {}
21:
22:  const Counter& Counter::operator++()
23:  {
24:      ++itsVal;
25:      return *this;
26:  }
27:
28:  const Counter Counter::operator++(int)
29:  {
30:      Counter temp(*this);
31:      ++itsVal;
32:      return temp;
33:  }
34:
35:  int main()
36:  {
37:      Counter i;
38:      std::cout << "The value of i is " << i.GetItsVal()
39:          << std::endl;
40:      i++;
41:      std::cout << "The value of i is " << i.GetItsVal()
42:          << std::endl;
43:      ++i;
44:      std::cout << "The value of i is " << i.GetItsVal()
45:          << std::endl;
46:      Counter a = ++i;
47:      std::cout << "The value of a: " << a.GetItsVal();
48:      std::cout << " and i: " << i.GetItsVal() << std::endl;
49:      a = i++;
50:      std::cout << "The value of a: " << a.GetItsVal();
51:      std::cout << " and i: " << i.GetItsVal() << std::endl;
52:      return 0;
53:  }


The value of i is 0
The value of i is 1
The value of i is 2
The value of a: 3 and i: 3
The value of a: 4 and i: 4
						

The postfix operator is declared on line 12 and implemented on lines 28–33. Note that the call to the prefix operator on line 43 does not include the flag integer (x), but is used with its normal syntax. The postfix operator uses a flag value (x) to signal that it is the postfix and not the prefix. The flag value (x) is never used, however.


operator+

The Increment operator is a unary operator, which means that it takes only one term. The addition operator (+) is a binary operator; two terms are used by the addition operator (a+b). How do you implement overloading the + operator for Count?


The goal is to be able to declare two Counter variables and then add them, as in this example:

Counter varOne, varTwo, varThree;
VarThree = VarOne + VarTwo;

Once again, you could start by writing a function, Add(), that would take a Counter as its argument, add the values, and then return a Counter with the result. Listing 14.4 illustrates this approach.

Listing 14.4. The Add() Function
 0:  // Listing 14.4
 1:  // Add function
 2:  #include <iostream>
 3:
 4:  class Counter
 5:  {
 6:  public:
 7:      Counter();
 8:      Counter(int initialValue);
 9:      ~Counter(){}
10:      int GetItsVal()const { return itsVal; }
11:      void SetItsVal(int x) {itsVal = x; }
12:      Counter Add(const Counter &);
13:
14:  private:
15:      int itsVal;
16:
17:  };
18:
19:  Counter::Counter(int initialValue):
20:  itsVal(initialValue)
21:  {}
22:
23:  Counter::Counter():
24:  itsVal(0)
25:  {}
26:
27:  Counter Counter::Add(const Counter & rhs)
28:  {
29:      return Counter(itsVal+ rhs.GetItsVal());
30:  }
31:
32:  int main()
33:  {
34:      Counter varOne(2), varTwo(4), varThree;
35:      varThree = varOne.Add(varTwo);
36:      std::cout << "varOne: " << varOne.GetItsVal()<< std::endl;
37:      std::cout << "varTwo: " << varTwo.GetItsVal() << std::endl;
38:      std::cout << "varThree: " << varThree.GetItsVal()
39:          << std::endl;
40:      return 0;
41:  }


varOne: 2
varTwo: 4
varThree: 6
						

The Add() function is declared on line 12. It takes a constant Counter reference, which is the number to add to the current object. It returns a Counter object, which is the result to be assigned to the left side of the assignment statement, as shown on line 35. That is: varOne is the object, varTwo is the parameter to the Add() function, and the result is assigned to varThree.


In order to create varThree without having to initialize a value for it, a default constructor is required. The default constructor initializes itsVal to 0, as shown on lines 23–25. Because varOne and varTwo need to be initialized to a non-zero value, another constructor is created, as shown on lines 19–21. Another solution to this problem is to provide the default value 0 to the constructor declared on line 8.

Overloading operator+

The Add() function itself is shown on lines 29–32. It works, but its use is unnatural. Overloading the operator + is a more natural use of the Counter class. Listing 14.5 illustrates this.

Listing 14.5. Operator+
 0:  // Listing 14.5
 1:  //Overload operator plus (+)
 2:  #include <iostream>
 3:
 4:  class Counter
 5:  {
 6:  public:
 7:      Counter();
 8:      Counter(int initialValue);
 9:      ~Counter(){}
10:      int GetItsVal()const { return itsVal; }
11:      void SetItsVal(int x) {itsVal = x; }
12:      Counter operator+ (const Counter &);
13:  private:
14:      int itsVal;
15:  };
16:
17:  Counter::Counter(int initialValue):
18:  itsVal(initialValue)
19:  {}
20:
21:  Counter::Counter():
22:  itsVal(0)
23:  {}
24:
25:  Counter Counter::operator+ (const Counter & rhs)
26:  {
27:      return Counter(itsVal + rhs.GetItsVal());
28:  }
29:
30:  int main()
31:  {
32:      Counter varOne(2), varTwo(4), varThree;
33:      varThree = varOne + varTwo;
34:      std::cout << "varOne: " << varOne.GetItsVal()<< std::endl;
35:      std::cout << "varTwo: " << varTwo.GetItsVal() << std::endl;
36:      std::cout << "varThree: " << varThree.GetItsVal()
37:          << std::endl;
38:      return 0;
39:  }


varOne: 2
varTwo: 4
varThree: 6
						

When the addition operator is invoked on line 33


varThree = varOne + varTwo;

the compiler interprets this as if you had written

varThree = varOne.operator+(varTwo);

and invokes the operator+ method declared on line 12 and defined on lines 25–28. Compare these with the declaration and definition of the Add() function in the previous listing; they are nearly identical. The syntax of their use, however, is quite different. It is more natural to say this:

varThree = varOne + varTwo;

than it is to say this:

varThree = varOne.Add(varTwo);

Not a big change, but enough to make the program easier to use and understand.

Limitations on Operator Overloading

Operators on built-in types (such as int) cannot be overloaded. The precedence order cannot be changed, and the arity of the operator, that is whether it is unary, binary, or trinary cannot be changed. You cannot make up new operators, so you cannot declare ** to be the “power of” operator.


What to Overload

Operator overloading is one of the aspects of C++ most overused and abused by new programmers. It is tempting to create new and interesting uses for some of the more obscure operators, but these invariably lead to code that is confusing and difficult to read.

Of course, making the + operator subtract and the * operator add can be fun, but no professional programmer would do that. The greater danger lies in the well-intentioned but idiosyncratic use of an operator—using + to mean concatenate a series of letters, or / to mean split a string. There is good reason to consider these uses, but there is even better reason to proceed with caution. Remember, the goal of overloading operators is to increase usability and understanding.

operator=

Remember that the compiler provides a default constructor, destructor, and copy constructor. The fourth and final function that is supplied by the compiler, if you don't specify one, is the assignment operator (operator=()).

This operator is called whenever you assign to an object. For example:

CAT catOne(5,7);
CAT catTwo(3,4);
// ... other code here
catTwo = catOne;

Here, catOne is created and in the constructor, the parameters (5,7) will be used to initialize the member variables itsAge and itsWeight respectively. catTwo is then created, assigned, and initialized with the values 3 and 4. Finally, you see the assignment operator (operator =)

catTwo = catOne;

The result of this assignment is that catTwo's itsAge and itsWeight values are assigned the values from catOne. Thus, after this statement executes, catTwo.itsAge will have the value 5 and catTwo.itsWeight will have the value 7.

Note, in this case the copy constructor is not called. catTwo already exists; there is no need to construct it, and so the compiler is smart enough to call the assignment operator.

In Hour 13, “Advanced Functions,” I discussed the difference between a shallow (member-wise) copy and a deep copy. A shallow copy just copies the members, and both objects end up pointing to the same area on the heap. A deep copy allocates the necessary memory. You saw an illustration of the default copy constructor in Figure 13.1; refer back to that figure if you need to refresh your memory.

You see the same issue here with assignment as you did with the copy constructor. There is an added wrinkle with the assignment operator, however. The object catTwo already exists and already has memory allocated. That memory must be deleted if there is to be no memory leak. So the first thing you must do when implementing the assignment oper ator is delete the memory assigned to its pointers. But what happens if you assign catTwo to itself, like this:

catTwo = catTwo;

No one is likely to do this on purpose, but the program must be able to handle it. More important, it is possible for this to happen by accident when references and dereferenced pointers hide the fact that the object's assignment is to itself.

If you did not handle this problem carefully, catTwo would delete its own memory allocation. Then, when it was ready to copy in the memory from the right-hand side of the assignment, it would have a very big problem: The memory would be gone.

To protect against this, your assignment operator must check to see if the right-hand side of the assignment operator is the object itself. It does this by examining the this pointer. Listing 14.6 shows a class with an assignment operator.

Listing 14.6. An Assignment Operator
 0:  // Listing 14.6
 1:  // Copy constructors
 2:  #include <iostream>
 3:
 4:  class CAT
 5:  {
 6:  public:
 7:      CAT(); // default constructor
 8:      // copy constructor and destructor elided!
 9:      int GetAge() const { return *itsAge; }
10:      int GetWeight() const { return *itsWeight; }
11:      void SetAge(int age) { *itsAge = age; }
12:      CAT operator=(const CAT &);
13:
14:  private:
15:      int *itsAge;
16:      int *itsWeight;
17:  };
18:
19:  CAT::CAT()
20:  {
21:      itsAge = new int;
22:      itsWeight = new int;
23:      *itsAge = 5;
24:      *itsWeight = 9;
25:  }
26:
27:
28:  CAT CAT::operator=(const CAT & rhs)
29:  {
30:      if (this == &rhs)
31:          return *this;
32:      delete itsAge;
33:      delete itsWeight;
34:      itsAge = new int;
35:      itsWeight = new int;
36:      *itsAge = rhs.GetAge();
37:      *itsWeight = rhs.GetWeight();
38:      return *this;
39:  }
40:
41:
42:  int main()
43:  {
44:      CAT frisky;
45:      std::cout << "frisky's age: " << frisky.GetAge()
46:          << std::endl;
47:      std::cout << "Setting frisky to 6...
";
48:      frisky.SetAge(6);
49:      CAT whiskers;
50:      std::cout << "whiskers' age: " << whiskers.GetAge()
51:          << std::endl;
52:      std::cout << "copying frisky to whiskers...
";
53:      whiskers = frisky;
54:      std::cout << "whiskers' age: " << whiskers.GetAge()
55:          << std::endl;
56:      return 0;
57:  }


frisky's age: 5
Setting frisky to 6;
whiskers' age: 5
copying frisky to whiskers...
whiskers' age: 6
						

Listing 14.6 brings back the CAT class, and leaves out the copy constructor and destructor to save room. On line 12, the assignment operator is declared, and on lines 28–39, it is defined.

On line 30, the current object (the CAT being assigned to) is tested to see if it is the same as the CAT being assigned. This is done by checking if the address of rhs is the same as the address stored in the this pointer.

Of course, the equality operator (==) can be overloaded as well, enabling you to determine for yourself what it means for your objects to be equal.

On lines 32–35 the member variables are deleted and then re-created on the heap. While this is not strictly necessary, it is good, clean programming practice and will save you from memory leaks when working with variable length objects that do not overload their assignment operator.


..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.226.104.27