Hour 15. Using Operator Overloading

Operator Overloading

The built-in types in C++ work with operators such as addition (+) and multiplication (*), making it easy to use these types in expressions:

int x = 17, y = 12, z;
z = x * (y + 5);

The C++ compiler knows to multiply and add integers when the * and + operators appear in an expression. The preceding code adds 5 to y, and then multiplies the result by x. The z integer is assigned the value 289.

A class could provide the same functionality with multiply() and add() member functions, but the syntax is a lot more complicated. Here’s a snippet of code for a Number class that represents integers and performs the same work as the preceding example:

Number x(17);
Number y(12);
Number z, temp;
temp = y.add(5);
z = x.multiply(temp);

This code adds 5 to y and multiplying the result by x. The result is still 289.

As you can see, the code is longer and more complex. For a simpler approach, classes can be manipulated with operators by using a technique called operator overloading.

Operator overloading defines what happens when a specific operator is used with an object of a class. Almost all operators in C++ can be overloaded.

Expressions written using operators are easier to read and understand.

For your first exploration of operator overloading, the Counter program in Listing 15.1 creates a class of that name. A Counter object will be used in counting, loops, and other tasks where a number must be incremented, decremented, or monitored.

Listing 15.1 The Full Text of Counter.cpp


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


This simple program creates a counter and displays its current value:

The value of c is 0

As it stands, this is pretty plain-vanilla stuff. The class is defined on lines 3–13 and has only one member variable, an int named value. The default constructor, which is declared on line 6 and implemented on lines 15–17, initializes the member variable to 0.

Unlike a built-in int, the Counter object can’t be incremented, decremented, added, assigned, or manipulated with operators. It can’t display its value easily, either.

The following sections address these shortcomings.

Writing an Increment Method

Operator overloading provides functionality that would otherwise be missing in user-defined classes such as Counter. When you implement an operator for a class, you are said to be overloading that operator.

The most common way to overload an operator in a class is to use a member function. The function declaration takes this form:

returnType operatorsymbol(parameter list)
{
    // body of overloaded member function
}

The name of the function is operator followed by the operator being defined, such as + or ++. The returnType is the function’s return type and the parameter list holds zero, one, or two parameters (depending on the operator).

The Counter2 program in Listing 15.2 illustrates how to overload the increment operator ++.

Listing 15.2 The Full Text of Counter2.cpp


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


This program increments the Counter object several times and creates a second object, displaying the values:

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

On line 35, you can see that the increment operator is invoked on an object of the Counter class:

++c;

This is interpreted by the compiler as a call to the implementation of operator++ shown on lines 21–25. This member function increments the member variable value and then dereferences the this pointer to return the current object. Because it returns the current object, it can be assigned to the variable a in line 38.

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 is not changed by the function using the object.

Overloading the Postfix Operator

The preceding project used the prefix version of the ++ increment operator, which raises the question of how the postfix operator could be overloaded. The prefix and postfix operators are both ++, so the name of the overloaded member function is not useful to distinguish between the two.

The way to handle this and overload the postfix operator is to include a int variable as the only parameter to the operator++() member function. The integer won’t be used; it’s just a signal that the function defines the postfix operator.

As you’ve learned in earlier hours, the prefix operator changes a variable’s value before returning it in expressions. The postfix operator returns the value before incrementing or decrementing it.

To do this, in an overloaded member function, a temporary object must be created to hold the original value while the value of the original object is incremented. The temporary object is returned because the postfix operator requires the original value, not the incremented value.

The temporary object must be returned by value and not by reference. Otherwise, it goes out of scope as soon as the function returns.

The Counter3 program in Listing 15.3 demonstrates how to overload the prefix and the postfix operators.

Listing 15.3 The Full Text of Counter3.cpp


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


This program overloads the prefix and postfix increment operators and uses them in several statements:

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

The postfix operator is declared on line 11 and implemented on lines 27–32. Note that the int parameter in the function declaration on line 27 is not used in any fashion. It isn’t even given a variable name.

Overloading the Addition Operator

The increment operator is a unary operator, which means that it takes only one operand. The addition operator (+) is a binary operator which adds two operands together, which adds a new wrinkle to how overloading works.

The next version of the Counter class will be able to add two Counter objects together using the + operator:

Counter var1, var2, var3;
var3 = var1 + var2;

Although you could write an add() method that takes two Counter objects and returns a Counter that contains their sum, a better technique is to overload the + operator. The Counter4 program in Listing 15.4 shows how to do this.

Listing 15.4 The Full Text of Counter4.cpp


 1: #include <iostream>
 2:
 3: class Counter
 4: {
 5: public:
 6:     Counter();
 7:     Counter(int initialValue);
 8:     ~Counter(){}
 9:     int getValue() const { return value; }
10:     void setValue(int x) { value = x; }
11:     Counter operator+(const Counter&);
12:
13: private:
14:     int value;
15: };
16:
17: Counter::Counter(int initialValue):
18: value(initialValue)
19: {}
20:
21: Counter::Counter():
22: value(0)
23: {}
24:
25: Counter Counter::operator+(const Counter &rhs)
26: {
27:     return Counter(value + rhs.getValue());
28: }
29:
30: int main()
31: {
32:     Counter alpha(4), beta(13), gamma;
33:     gamma = alpha + beta;
34:     std::cout << "alpha: " << alpha.getValue() << " ";
35:     std::cout << "beta: " << beta.getValue() << " ";
36:     std::cout << "gamma: " << gamma.getValue()
37:         << " ";
38:     return 0;
39: }


The program adds two Counter objects, storing the sum in a third:

alpha: 4
beta: 13
gamma: 17

As you can see from the output, the gamma object contains the sum of alpha plus beta.

The addition operator is invoked on line 33:

gamma = alpha + beta;

The compiler interprets the statement as if you had written the following code:

gamma = alpha.operator+(beta);

Line 33 invokes the operator+ member function declared on line 11 and defined on lines 25–28.

There are two operands in an addition expression. The left operand is the object whose operator+() function is called. The right operand is the parameter of this method.

If you had written an add() method to add two objects together, it could have been called with a statement of this kind:

gamma = alpha.add(beta);

Operator overloading makes programs easier to use and understand by replacing explicit function calls.

Limitations on Operator Overloading

Although operator overloading is one of the most powerful features in the C++ language, it has limits.

Operators for built-in types such as int cannot be overloaded. The precedence order cannot be changed, and the arity of the operator—whether it is unary, binary, or trinary—cannot be altered, either. You also cannot make up new operators, so there’s no way to do something such as declaring ** to be the exponentiation (power of) operator.

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 often lead to code that is confusing and difficult to read.


Watch Out!

Doing counterintuitive things like making the + operator subtract and the * operator add is amusing the first time you try it, but no pros would do that in their code.

The real danger lies in the well-intentioned but idiosyncratic use of an operator, such as using + to concatenate a series of letters or / to split a string. There is good reason to consider these uses, but better reason to proceed with caution. The goal of overloading operators is to increase usability and understanding.


operator=

The C++ compiler provides each class with a default constructor, destructor, and copy constructor. A fourth member function supplied by the compiler, when one has not been specified in the class, defines the assignment operator.

The assignment operator’s overloaded function takes the form operator=() and is called when you assign a value to an object, as in this code:

Tricycle wichita;
wichita.setSpeed(4);
Tricycle dallas;
dallas.setSpeed(13);
dallas = wichita;

The Tricycle object named wichita is created and its member variable speed given the value 4, followed by the Tricycle dallas with the value 13. The final statement uses the assignment operator =.

Because of this assignment, dallas’s speed variable is assigned the value of that variable from wichita. After this statement executes, dallas.speed will have the value 4 rather than 13.

In this case, the copy constructor is not called because dallas already exists, so there’s no need to construct it. The compiler calls the assignment operator instead.

Hour 14, “Creating Advanced Functions,” described the difference between a shallow (member-wise) copy and a deep copy. A shallow copy just copies the members, making both objects point to the same area on the heap. A deep copy allocates the necessary memory.

The same issue crops up here, with an added wrinkle. Because the object dallas already exists and has memory allocated, that memory must be deleted to prevent a memory leak.

For this reason, the first thing you must do when overloading the assignment operator is delete the memory assigned to its pointers with statements such as this:

delete speed;

This works, but what happens if you assign dallas to itself:

dallas = dallas;

No programmer is likely to do this on purpose, but the class must be able to handle this situation because it can happen by accident. References and dereferenced pointers might hide the fact that an object is being assigned to itself.

If you don’t guard against this problem, the self-assignment causes dallas to delete its own memory allocation. After it does, when it’s ready to copy the memory from the right side of the assignment, that memory is gone.

This can be prevented if the assignment operator checks to see whether the right side of the assignment operator is the object itself using the this pointer.

The Assignment class in Listing 15.5 uses overloading to define a custom assignment operator and avoids the same-object problem.

Listing 15.5 The Full Text of Assignment.cpp


 1: #include <iostream>
 2:
 3: class Tricycle
 4: {
 5: public:
 6:     Tricycle();
 7:     // copy constructor and destructor use default
 8:     int getSpeed() const { return *speed; }
 9:     void setSpeed(int newSpeed) { *speed = newSpeed; }
10:     Tricycle operator=(const Tricycle&);
11:
12: private:
13:     int *speed;
14: };
15:
16: Tricycle::Tricycle()
17: {
18:     speed = new int;
19:     *speed = 5;
20: }
21:
22: Tricycle Tricycle::operator=(const Tricycle& rhs)
23: {
24:     if (this == &rhs)
25:          return *this;
26:     delete speed;
27:     speed = new int;
28:     *speed = rhs.getSpeed();
29:     return *this;
30: }
31:
32: int main()
33: {
34:     Tricycle wichita;
35:     std::cout << "Wichita's speed: " << wichita.getSpeed()
36:         << " ";
37:     std::cout << "Setting Wichita's speed to 6 ... ";
38:     wichita.setSpeed(6);
39:     Tricycle dallas;
40:     std::cout << "Dallas' speed: " << dallas.getSpeed()
41:         << " ";
42:     std::cout << "Copying Wichita to Dallas ... ";
43:     wichita = dallas;
44:     std::cout << "Dallas' speed: " << dallas.getSpeed()
45:         << " ";
46:     return 0;
47: }


Assignment produces this output when run:

Wichita's speed: 5
Setting Wichita's speed to 6 ...
Dallas' speed: 5
Copying Wichita to Dallas ...
Dallas' speed: 5

Listing 15.5 brings back the Tricycle class, omitting the copy constructor and destructor to save room. On line 10, the assignment operator is declared, and on lines 22–30, it is defined.

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

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


By the Way

On lines 26–27 the member variable speed is deleted and re-created on the heap. Although this is not strictly necessary, it is good programming practice that avoids memory leaks when working with variable-length objects that do not overload their assignment operators.


Conversion Operators

What happens when you try to assign a variable of a built-in type, such as int or unsigned short, to an object of a user-defined class? Listing 15.6 brings back the Counter class and attempts to assign a variable of type int to a Counter object.


Watch Out!

Listing 15.6 will not compile, for reasons you’ll learn after preparing it.


Listing 15.6 The Full Text of Counter5.cpp


 1: #include <iostream>
 2:
 3: class Counter
 4: {
 5: public:
 6:     Counter();
 7:     ~Counter() {}
 8:     int getValue() const { return value; }
 9:     void setValue(int newValue) { value = newValue; }
10: private:
11:     int value;
12: };
13:
14: Counter::Counter():
15: value(0)
16: {}
17:
18: int main()
19: {
20:     int beta = 5;
21:     Counter alpha = beta;
22:     std::cout << "alpha: " << alpha.getValue() << " ";
23:     return 0;
24: }


When you attempt to compile this program, it fails with an error about trying to convert an int to a Counter object in line 21.

The Counter class declared on lines 3–12 has only a default constructor. It declares no particular member function for turning an int into a Counter object, so line 21 triggers a compile error. The compiler cannot figure out, absent such a function, that an int should be assigned to the object’s member variable value.

The Counter6 program (Listing 15.7) corrects this by creating a conversion operator: a constructor that takes an int and produces a Counter object.

Listing 15.7 The Full Text of Counter6.cpp


 1: #include <iostream>
 2:
 3: class Counter
 4: {
 5: public:
 6:     Counter();
 7:     ~Counter() {}
 8:     Counter(int newValue);
 9:     int getValue() const { return value; }
10:     void setValue(int newValue) { value = newValue; }
11: private:
12:     int value;
13: };
14:
15: Counter::Counter():
16: value(0)
17: {}
18:
19: Counter::Counter(int newValue):
20: value(newValue)
21: {}
22:
23: int main()
24: {
25:     int beta = 5;
26:     Counter alpha = beta;
27:     std::cout << "alpha: " << alpha.getValue() << " ";
28:     return 0;
29: }


This code compiles successfully and produces the following line of output:

alpha: 5

The important change is on line 8, where the constructor is overloaded to take an int, and on lines 19–21, where the constructor is implemented. The effect of this constructor is to create a Counter out of an int.

Given this constructor, the compiler knows to call it when an integer is assigned to a Counter object in line 26.

The int() Operator

The preceding project demonstrated how to assign a built-in type to an object. It’s also possible to assign an object to a built-in type, which is attempted in this code:

Counter gamma(18);
int delta = gamma;
cout << "delta : " << delta << " ";

If this code were added to the Counter6 program, it would not compile successfully. The class knows how to create a Counter from an integer, but it does not know how to accomplish the reverse and create an integer from a Counter.

C++ provides conversion operators that can be added to a class to specify how to do implicit conversions to built-in types. The Counter7 program in Listing 15.8 illustrates this.

Listing 15.8 The Full Text of Counter7.cpp


 1: #include <iostream>
 2:
 3: class Counter
 4: {
 5: public:
 6:     Counter();
 7:     ~Counter() {}
 8:     Counter(int newValue);
 9:     int getValue() const { return value; }
10:     void setValue(int newValue) { value = newValue; }
11:     operator unsigned int();
12: private:
13:     int value;
14: };
15:
16: Counter::Counter():
17:value(0)
18: {}
19:
20: Counter::Counter(int newValue):
21: value(newValue)
22: {}
23:
24: Counter::operator unsigned int()
25: {
26:     return (value);
27: }
28:
29: int main()
30: {
31:     Counter epsilon(19);
32:     int zeta = epsilon;
33:     std::cout << "zeta: " << zeta << " ";
34:     return 0;
35: }


Counter7 produces the following output when run:

zeta: 19

On line 11, the conversion operator is declared. Note that it has no return value. The implementation of this function is on lines 24–27. Line 26 returns the value of the object’s value member variable. The integer returned by the function matches the type in the function declaration.

Now the compiler knows how to turn integers into Counter objects and vice versa, so they can be assigned to one another freely.

Note that conversion operators do not specify a return value, despite the fact that they are returning a converted value.

Summary

Operator overloading is one of the most powerful aspects of the C++ language. By defining how operators behave in the classes that you design, you make it easier to work with objects of those classes.

Almost all operators in C++ can be overloaded.

As you have seen in working with built-in types, using operators to manipulate objects is considerably easier than calling member functions. It also results in programs that are easier to comprehend.

This assumes, of course, that the behavior of overloaded operators is consistent with how they work on built-in types.

Q&A

Q. Why would you overload an operator when you can just create a member function?

A. It is easier to use overloaded operators when their behavior is well understood. Less code is required to accomplish the same task, and your classes can mimic the functionality of the built-in types.

Q. What is the difference between the copy constructor and the assignment operator?

A. The copy constructor creates a new object with the same values as an existing object. The assignment operator changes an existing object so that it has the same values as another object.

Q. What happens to the int used in the postfix operators?

A. Nothing. That int is never used, except as a flag to overload the postfix and prefix operators.

Q. Who was Alanis Morissette singing about in “You Oughta Know”?

A. The incendiary breakup song from her 1995 album Jagged Little Pill, which reached No. 1 and has sold more than 33 million copies, was about a real person she dated. Morissette admitted that much in interviews but has never publicly identified the person.

The actor Dave Coulier, who played Joey in the sitcom Full House, broke up with Morissette shortly before the release of the album—making him the No. 1 suspect.

In 2008, Coulier told the Calgary Herald that the song was about him. Describing what it was like when he first heard it on the radio, Coulier revealed, “I said, ‘Wow, this girl is angry.’ And then I said, ‘Oh man, I think it’s Alanis.’ I listened to the song over and over again, and I said, ‘I think I have really hurt this person.’”

He oughta know.

Workshop

Now that you’ve worked with overloaded operators, you can answer a few questions and do a couple of exercises to firm up your knowledge of the hour.

Quiz

1. Why can’t you create totally new operators like ** for exponentiation?

A. That operator isn’t part of the language.

B. Because it uses an existing operator, *.

C. You can create new operators.

2. Why is the overload syntax different for prefix and postfix increment and decrement operations?

A. Because prefix and postfix return different values.

B. Because one uses ++ and the other uses --.

C. The syntax is not different.

3. What do conversion operators do?

A. Convert objects to built-in types

B. Convert built-in types to objects

C. Both a and b

Answers

1. A. Adding new operators such as ** requires a change to the compiler, because ** is not part of the language and so the compiler would not know what to do with it.

2. A. The behavior differs totally depending on whether the ++ or -- operator appears before or after a variable, so the code must follow the same behavior. Technically, it doesn’t have to mimic the behavior, but users of your class will expect it to work that way.

3. A. They convert from the object type to a built-in type.

Activities

1. Modify the Assignment program (Listing 15.6) to overload the equality operator (==). Use that operator to compare two Tricycle object’s speeds.

2. Modify the Counter2 program (Listing 15.5) to also overload the minus operator and use it to perform simple subtraction.

To see solutions to these activities, visit this book’s website at http://cplusplus.cadenhead.org.

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

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