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.
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.
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 ++
.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
int()
OperatorThe 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.
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.
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. 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.
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.
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
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.
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.
3.144.116.69