CS202 Programming
Systems
Lecture Notes #3
Building a C++ Foundation
with
Operator
Overloading
Lecture
and Reference
Operator Overloading: Defining the behavior of C++ operators when one or more of the operands are objects of a class.
So far, we have designed classes consisting of data members and member functions. We have defined constructors, copy constructors, destructors, and member functions to support the necessary operations. With operator overloading we can implement operations using operators instead of member functions.
Operator overloading allows us to define the behavior of operators when they are applied to objects of our classes. We might implement member functions to read employee information from standard input and to write employee information to standard output. With this implementation, the client had to explicitly invoke the member functions read and write to perform I/O. By modifying the implementation and overloading the extraction (>>) and insertion (<<) operators to perform these same tasks, the client can use these operators in the same way as they are used with the built-in types. Instead of calling the read member function, the >> operator would be used to extract an object's data from standard input. This can't happen without operator overloading becasue the iostream library is not aware of the types of objects that we create. Therefore, we must define how the extraction and insertion operators should behave when usesd with objects of our own classes. Instead of calling the write member function, the << operator would be used to insert an object's data into the output stream. This is illustrated in the following example:
employee emp; //employee object
//using member functions
emp.read(); //input employee information
emp.write(); //output employee information
//using operator overloading
cin >> emp; //input employee information
cout << emp; //output employee information
Notice how natural this is. We are performing operations on objects of our classes in the same way that we perform those operations on objects of types built into the language.
3.1 Syntax
Operator overloading is built around the notion that we can only redefine the meaning of operators when they are applied to objects of our classes. Operator overloading does not allow us to alter the meaning of operators when applied to types built into the language; that meaning is defined by the language. We can only redefine the meaning of operators for types that we create.
The following sections describe the basics of operator overloading. We look at how to declare overloaded operators, how to define them, and finally how to use them. Once this is understood, we are ready to look at restrictions and key concepts assocated with operator overloading.
3.1.1 Declaring Overloaded Operators
Overloading operators is similar to overloading functions, except that the function name is replaced by the keyword operator followed by the operator's symbol. Think of an overloaded operator as a function who's name identifies the operator being redefined. No other name is needed. Operations to add or subtract could be implemented as operators, where the "function" name is operator+ or operator-.
To undersand an operator's declaration, examine Figure 3-1. This declaration allows us to apply the subtraction operator to two objects of the same class and returns an object of that class as an rvalue. The italics represent our recommendations for those parts of the declaration that are not required, but if followed, result in behavior that more closely matches that of the built-in types. Since the predefined behavior the subtraction operator does not modify its two operands, the formal arguments of the operator- function should be specified either as constant references or passed by value.
Figure 3-1: An Example of an Overloaded Operator's Declaration
An overloaded operator's operands are defined the same as arguments are defined for functions. The arguments represent the operator's operands. Unary operators have a single argument and binary operators have two arguments. When an operator is used, the operands become the actual arguments of the "function call". Therefore, the formal arguments must match the data type(s) expected as operands or a conversion to those types must exist.
Examine the function prototype illustrated in Figure 3-1. Since there are two arguments, we know that the binary subtraction operator is being overloaded rather than the unary minus operator. We also know that both operands are expected to be objects of a class. And, since they are specified as constant references, we immediately know that the actual arguments can be specified as constant or non-constant objects and are themselves not modified by this operator.
The return type of overloaded operators is also defined the same as it is for overloaded functions. The value returned from an overloaded operator is the residual value of the expression containing that operator and its operands. It is extremely important that we pay close attention to the type and value returned. It is the returned value that allows an operator to be used within a larger expression. It allows the result of some operation to become the operand for another operator. A return type of void would render an operator useless when used within an expression. Therefore, we recommend that all operators return a value of the appropriate type (i.e., not void).
Declaring an overloaded operator is the same as declaring an overloaded function. To demonstrate these similarities, consider a simple class for complex number subtraction. In this example, we have defined two functions to perform the same task: one as a regular function called subtract and another as the subtraction operator (operator-). The declaration of both of these functions is identical; the only difference is the function name.
//complex.h (Ex1301)
class complex {
public:
explicit complex(double r=0, double i=0) :
real(r), imag(i) {
}
double real; //real component
double imag; //imaginary component
};
//function prototypes
complex subtract (const complex &, const complex &);
complex operator- (const complex &, const complex &);
3.1.2 Defining Overloaded Operators
Defining an overloaded operator is the same as defining a function. In the following example, we have implemented the two functions declared in the previous section. The code for both of these functions is identical; the only difference is in the name.
//complex.cpp (Ex1301)
#include "complex.h"
//define subtract function for complex class
complex subtract(const complex &c1, const complex &c2) {
complex temp;
temp.real = c1.real - c2.real;
temp.imag = c1.imag - c2.imag;
return (temp);
}
//define overloaded operator- function for complex class
complex operator-(const complex &c1, const complex &c2) {
complex temp;
temp.real = c1.real - c2.real;
temp.imag = c1.imag - c2.imag;
return (temp);
}
Practical Rule: Remember, we are not allowed to overload the predefined meaning of the operators on types built into the language. Operator overloading is only permitted if at least one operand is an object of a user defined type.
3.1.3 Using Overloaded Operators
Using an overloaded operator rather than a regular function is different. Instead of calling a function (e.g., result = subtract(c1,c2)), we simply use the operator in the same way that we would if we were using a built-in type. Figure 3-2 illustrates how a client might use such an operator. The first operand corresponds to the first formal argument of the overloaded operator and the second operand corresponds to the second formal argument of the overloaded operator. This use is straightforward if the operator is designed consistently with the predefined behavior for that operator.
In this case, the behavior is consistent. We can allow either operand to be specified as a constant object, which means that we do not expect the contents of either operand to be modified. And the residual value is the result of adding the two operands together.
Figure 3-2: Using an Overloaded Operator
Using the operator with its operands calls the overloaded function. We simply apply the operator to operands of the type that is expected. The operands correspond to the actual arguments of a function call. The value returned from the overloaded operator function is the residual value of the expression containing the operator and it operands. The following example compares a regular function call with an overloaded operator doing the same thing.
//main.cpp (Ex1301)
#include <iostream>
using namespace std;
#include "complex.h"
int main() {
complex c1(4.0, 3.0);
complex c2(1.0, 2.0);
complex c;
c = subtract(c1, c2); //explicit subtract function call
cout <<"(" <<c.real <<", " <<c.imag <<")" <<endl;
c = c1 - c2; //implicit operator- function call
cout <<"(" <<c.real <<", " <<c.imag <<")" <<endl;
return (0);
}
This results in the following output:
(3, 1)
(3, 1)
We can also explicitly call an overloaded operator. While this is possible, it is not common practice. For an explicit call, we use the name of the function (operator-) just like a regular function call:
c = operator-(c1, c2); //explicit operator- function call
3.2 Restrictions
Operator overloading allows us to define the behavior of existing operators when they are applied to objects of classes as specified in Table 3-1. Operator overloading does not allow us to define our own operators, alter the precedence and associativity of the operators, or change the number of operands associated with a particular operator. This means that operator overloading is not designed to allow us to extend the set of operators available in C++. It does not allow us to change the meaning of operators for built-in types. In addition, default arguments are not allowed. Imagine performing addition with only one operand assuming that the other operand would take on a default value!
• Operators must come from the built-in operators. We cannot define our own operators.
• Operators maintain their precedence and associativity. We cannot alter it for our own classes.
• Operators must be overloaded to expect the correct number of operands. Unary operators expect one operand and binary operators expect two operands.
• Operators can only be overloaded when at least one of the operands is a object of a class. We cannot redefine the operation of operators on built-in types.
• Operators cannot have default arguments.
Table 3-1: Restrictions when Overloading Operators
Operator overloading allows us to implement code so that operators perform as expected when used with objects of classes. We are expected to keep the implementation of the operators consistent with their predefined behavior. By doing o, we can use objects more in the same contexts as built-in types than what we have seen so far.
All arithmetic, bitwise, relational, equality, logical, and compound assignment operators can be overloaded. In addition, the address-of, dereference, increment, decrement, and comma operators can be overloaded. However, beyond this there are restrictions. The following sections highlight which operators cannot be overloaded and which operators have restrictions.
Practical Rule: The meaning of an overloaded operator should be consistent with how that operator is used with built-in data types.
3.2.1 Operators That Cannot Be Overloaded
There are twelve operators that cannot be overloaded, as shown in Table 3-1.
:: scope resolution operator
. direct member access operator
.* direct pointer to member access operator
?: conditional operator
sizeof size of object operator
new memory allocation operator
delete memory deallocation operator
static_cast cast operator
const_cast cast operator
reinterpret_cast cast operator
dynamic_cast run time type identification cast operator
typeid run time type identification type operator
Table 3-2: Operators that Cannot be Overloaded
The new and delete operators are different than operator new and operator delete. The new and delete operators cannot be overloaded whereas operator new and operator delete can be overloaded. The new and delete operators ensure that enough space is allocated to hold an object and that the constructor for the object is called. On the other hand, operator new and operator delete are the functions that are called by the new and delete operators to allocate the space. Those functions can be overloaded.
3.2.2 Operators That Must Be Member Functions
There are five operators that can be overloaded, but only as member functions. All five of these operators are binary operators and are listed in Table 3-3. The first operand must be an object of the class.
= assignment operator
[] subscript operator
() function call operator
-> indirect member access operator
->* indirect pointer to member access operator
Table 3-3: Operators that must be Overloaded as Member Functions
3.2.3 Unary and Binary Operators
Operators are either unary, binary, or ternary. A unary operator takes one operand, a binary operator takes two operands, and the ternary operator takes three operands. Both the unary and binary forms of the &, *, + and - operators can be overloaded. The one ternary operator (?:) cannot be overloaded.
& address of operator
* dereference operator
++ increment operator (both prefix and postfix)
-- decrement operator (both prefix and postfix)
+ plus operator
- minus operator
~ bitwise negation operator (complement)
! logical negation operator
Table 3-4: Unary Operators Overloadable as Members or Non-members
* / % + - arithmetic operators
<< >> & ^ | bitwise operators
< <= > >= relational operators
== != equality operators
&& || logical operators
*= /= %= += -= arithmetic assignment operators
<<= >>= &= ^= |= bitwise assignment operators
, comma operator
Table 3-5: Binary Operators Overloadable as Members or Non-members
Even though the operators listed can be overloaded as either member or non-member functions, there are good reasons to always overload certain operators as one or the other. As we discuss each of the operators, we will give guidelines about whether they should be overloaded as member or non-member functions.
3.2.4 Operators
Predefined for Each Class
Three operators are predefined by the compiler for each class that we create. These are listed in Table 3-6. These operators have their usual meanings and are implicitly overloaded for a class when no explicit overloaded operator is defined. Each can be overloaded if the predefined behavior is not sufficient (e.g., if a deep copy needs to be performed for the assignment operator instead of a memberwise copy).
& address of operator
= assignment operator
, comma operator
Table 3-6: Operators Implicitly Defined for Each Class
3.2.5 General Guidelines
Some general guidelines must be followed in order to overload operators so that they behave in the same way as the built-in types. These are summarized in Table 3-7.
Class operations can be implemented either as member functions or as operators. The first step is to determine if any of the class operations should be implemented as overloaded operators. Once that is determined, then a natural association between each operation and its associated overloaded operator should be established. For example, if we were defining a complex number class, we would want to overload the arithmetic operations as operators. For each operation there is a natural association with an operator, such as complex number subtraction and the - operator.
As we design our abstractions, we should ask ourselves if an operator exists that performs behavior similar in nature to our operations. If so, consider overloading those operators. If not, consider implementing these operations as member functions. Programmers expect operators to behave in a consistent way, regardless of the data type. Programmers do not expect to have to read a manual to determine how to use an operator!
We believe that it is important to keep the meaning of the operators intuitive. That is, the meaning of an operator when applied to an object of a class we create should behave in a predictable way based on common usage of the operators. For example, it would be unreasonable for the + operator to perform subtraction on complex numbers! It would be equally strange if programmers who are used to chaining insertion operators together (i.e., cout <<i <<j <<k) were all of a sudden not able to do so when using an object of a class! We must be very careful when overloading operators to ensure consistent behavior.
Understanding an operator's behavior means much more than simply understanding what operation is being performed. It also means that we must understand what data types are allowed as operands, what conversions can be applied to the operands, whether or not the operands are modified by the operation that takes place, what data type is returned as the residual value, and whether the residual value is an rvalue (an object returned by value), a non-modifiable lvalue (a const reference to an object), or a modifiable lvalue (a reference to an object). Understanding each of these is critical to achieving consistent and intuitive behavior.
Not only should we make sure that the behavior is consistent, but a complete set of overloaded operators should be provided. For example, if we overload the subtraction and assignment operators, we should also overload the subtraction compound assignment (-=) operator to be consistent. If we overload the iostream extraction operator, we may also need to overload the iostream insertion operator.
When overloading operators, we are in complete control of the operations being performed, what data types to expect as operands, and what is returned as the residual value. Therefore, it takes careful planning not only to decide which operators to overload, but to overload them in a way that is intuitive and natural for the client and consistent with the behavior of these operators on the built-in types.
• Determine if any of the class operations should be implemented as overloaded operators instead of member functions.
• Be sure that the operators provided for our class make sense to the abstraction being defined. There should be a natural association between the operation being performed and the operator being used.
• Be consistent with how the operators work with the built-in types. Understand what data types are allowed as operands, what conversions can be applied to the operands, whether or not the operands are modified, what data type is returned as the residual value, and whether the residual value is an rvalue, a non-modifiable lvalue, or a modifiable lvalue.
• Provide a complete set of overloaded operators for each class.
Table 3-7: General Guidelines
3.3 Principles
Before we begin overloading operators, we should understand a set of fundamental principles. These principles show us when to overload operators as member functions and how to overload operators as non-member functions. They show us the importance of operator signatures and operator overload resolution. We will understand how implicit user defined conversions can be applied to operators that we overload and how they affect operator overload resolution.
3.3.1 Overloading Operators as Members
When we overload operators as members, the first operand must be an object of the class. Because of this, unary operators are usually overloaded as members. When the overloaded operator is called, the this pointer points to that object. Since binary operators expect two operands, they can be overloaded as either member or non-member functions depending on their usage. Operators overloaded as member functions have access to all of the private data members of their class.
For unary operators, the single operand is an object of the class and no arguments are expected. For binary operators, the first operand is an object of the class and the second operand is specified as an explicit argument. It may be either an object of the class or some other type.
Figure 3-3 illustrates the syntax for unary operators overloaded as members by overloading the unary minus operator. Figure 3-4 illustrates the syntax for binary operators overloaded as members by overloading the binary subtraction operator. Remember that the italics in these figures represent our recommendations for those parts of the declaration that are not required, but if followed, result in behavior that more closely matches that of the built-in types.
Figure 3-3: Declaration of Unary Operators Overloaded as Members
Figure 3-4: Declaration of Binary Operators Overloaded as Members
When the first operand is not modified by the operator, we recommend overloading the operator as a constant member function. Since neither the unary minus or the binary subtraction operators modify the first operand, we have overloaded them as constant functions.
For binary operators, when the second operand is not modified by the operator, the corresponding formal argument should either be passed by value or be specified as a constant reference. When the second operand is a built-in type, use pass by value. When the second operand is an object of a class, use constant references because they do not require use of the copy constructor.
The following class demonstrates the syntax for overloading both unary and binary operators as members of a class. Notice that the implementation of both operators must be preceded by the class name and the scope resolution operator, just like any member function, when their implementation is not part of the class definition itself.
//complex.h (Ex1302)
class complex {
public:
explicit complex(double r=0, double i=0) :
real(r), imag(i) {
}
complex operator-() const; //unary
complex operator-(const complex &) const; //binary
private:
double real; //real component
double imag; //imaginary component
};
//unary member function
inline complex complex::operator-() const {
complex temp;
temp.real = -real;
temp.imag = -imag;
return (temp);
}
//binary member function
inline complex complex::operator-(const complex &c) const {
complex temp;
temp.real = real - c.real;
temp.imag = imag - c.imag;
return (temp);
}
Practical Rule: Operators overloaded as members should be specified as constant functions whenever the first operand is not modified.
3.3.2 Overloading Operators as Non-members
Overloading operators as non-member functions is like defining regular C++ functions. Since they are not part of a class' definition, they can only access the public members. They have no access to the private data members. Because of this, non-member overloaded operators are often declared to be friends of the class. When we overload operators as non-member functions, all operands must be explicitly specified as formal arguments. Unary operators have a single argument which must be an object of a class. Binary operators have two arguments. Either the first or the second must be an object of a class; the other operand can be any type.
Figure 3-5 illustrates the syntax for unary operators overloaded as non-member functions by overloading the unary minus operator. Figure 3-6 illustrates the syntax for binary operators overloaded as non-member functions by overloading the binary subtraction operator.
Figure 3-5: Declaration of Unary Operators Overloaded as Non-members
Figure 3-6: Declaration of Binary Operators Overloaded as Non-members
When an operand is not modified by the operator, the corresponding formal argument should either be passed by value or be specified as a constant reference. When the operand is an object of a class, constant references are more efficient because they do not require the use of the copy constructor.
The following class demonstrates the syntax for overloading both unary and binary operators as non-members. Since the data members have been specified as private, these operators are declared to be friends to allow them to access the private data of the class.
//complex.h (Ex1303)
class complex {
friend complex operator-(const complex &); //unary
friend complex operator-(const complex &,
const complex &); //binary
public:
explicit complex(double r=0, double i=0) :
real(r), imag(i) {
}
private:
double real; //real component
double imag; //imaginary component
};
//unary non-member function
inline complex operator-(const complex &c) {
complex temp;
temp.real = -c.real;
temp.imag = -c.imag;
return (temp);
}
//binary non-member function
inline complex operator-(const complex &c1,
const complex &c2) {
complex temp;
temp.real = c1.real - c2.real;
temp.imag = c1.imag - c2.imag;
return (temp);
}
3.3.3 Operator Signatures and Candidate Operator Functions
When we overload an operator, we are defining a new signature for that operator. The signature depends on the particular operator and the type(s) of its formal argument(s). When that operator is used in an expression, the operands become the actual arguments of the overloaded operator function call. The overloaded operator function whose signature is the best match is used.
The process of determining the best match is called operator overload resolution and is the same as we described for overloaded. The only difference is in the list of candidate functions. For overloaded operators, the candidate functions consist of all operator functions defined for the particular operator being used. These operator functions can be either non-member functions, member functions, or built-in candidates.
The signature of operator functions that are not members of a class is the same for regular C++ functions. The name of the function consists of the keyword operator followed by the symbol for the operator instead of a user specified identifier. The following is the signature for the non-member binary operator- function in our complex class:
operator-(const complex &, const complex &);
The signature of operator functions that are members of a class is the same as non-members except that one additional argument is added as the implied first argument of the formal argument list. This argument is called the implied object argument. Its type is a reference to an object of the type of class that the overloaded operators are a member of. The following is the signature for the member binary operator- function of our complex class with the implied object argument added:
operator-(const complex &, const complex &);
If the overloaded operator functions are declared to be const, then the implied object argument is a const reference to an object of that type. The purpose of the implied object argument is to participate in operator overload resolution. When the first operand is an object of a class, any member functions overloaded for that operator in that class become candidate functions, otherwise no member function is a candidate. The first operand of the operator is compared with the type of the implied object argument for the candidate member operator functions.
Since the operator- member function was declared to be const, the first formal argument is also a const reference. If the member operator- function had not been declared to be const, then the implied object argument (the first argument) would simply be complex &. Notice that the signature of the operator- non-member function is the same as the member function. If both were defined for the same class, an ambiguity would result when it was used with that signature. The following example shows such an ambiguity and is illegal:
//complex.h (Ex1304)
class complex {
friend complex operator-(const complex &,
const complex &);
public:
explicit complex(double r=0, double i=0) :
real(r), imag(i) {
}
complex operator-(const complex &) const;
private:
double real; //real component
double imag; //imaginary component
};
inline complex complex::operator-(const complex &c) const {
complex temp;
temp.real = real - c.real;
temp.imag = imag - c.imag;
return (temp);
}
inline complex operator-(const complex &c1,
const complex &c2) {
complex temp;
temp.real = c1.real - c2.real;
temp.imag = c1.imag - c2.imag;
return (temp);
}
//main.cpp (Ex1304)
#include "complex.h"
int main() {
complex c(1.0, 2.0);
c = c - c; //ambiguous access to overloaded operator
return (0);
}
This ambiguity can be eliminated by removing one of the two operator functions. An overloaded operator function should be overloaded either as a member or a non-member function, not both.
The last set of candidate functions for an overloaded operator are the operator functions defined for the built-in types. Each possible type of operand (discounting promotions and conversions) defines a built-in operator function. For any given operator, these are added to the list of candidate functions. Their only purpose is to participate in operator overload resolution. For the subtraction operator, the following are the signatures for the built-in candidate functions:
operator-(double, double)
operator-(unsigned long, unsigned long)
operator-(long, long)
operator-(unsigned int, unsigned int)
operator-(int, int)
The candidate functions for any given overloaded operator consist of the non-member operator functions, member operator functions, and built-in operator functions for that particular operator. From this list of candidate functions, the best viable operator function is selected according to the overload resolution process (Refer to the Lecture on Functions).
3.3.4 Implicit Conversions
Implicit user defined conversions are often used to reduce the number of overloaded operators that need to be implemented. The following section discusses when these implicit conversions can be helpful. We then discuss issues to be aware of when providing implicit user defined conversions in conjunction with overloaded operators.
3.3.4.1 Symmetric Operations
Symmetric operators, such as addition and subtraction, are best defined as non-member operator functions. The member form requires that the first operand be an object of the class. The non-member form does not require that an object of the class be the first operand. Therefore, for the non-member form, all combinations of argument types can be overloaded. For example, if we wanted to allow mixed mode expressions containing both complex and double types, we could only overload two of the three possible combinations using member functions. However, if we used a non-member function, then we could overload all three combinations. The following example illustrates this:
//complex.h (Ex1305)
class complex {
friend complex operator-(const complex &,
const complex &);
friend complex operator-(const complex &, double);
friend complex operator-(double, const complex &);
public:
explicit complex(double r=0, double i=0) :
real(r), imag(i) { }
private:
double real; //real component
double imag; //imaginary component
};
inline complex operator-(const complex &c1,
const complex &c2) {
complex temp;
temp.real = c1.real - c2.real;
temp.imag = c1.imag - c2.imag;
return (temp);
}
inline complex operator-(const complex &c1, double d) {
complex temp;
temp.real = c1.real - d;
temp.imag = c1.imag - 0;
return (temp);
}
inline complex operator-(double d, const complex &c2) {
complex temp;
temp.real = d - c2.real;
temp.imag = 0 - c2.imag;
return (temp);
}
//main.cpp (Ex1305)
#include "complex.h"
int main() {
complex c(1.0, 2.0);
double d(3.14159);
c = c - c; //okay if operator- is member or non-member
c = c - d; //okay if operator- is member or non-member
c = d - c; //not allowed if operator- is member
return (0);
}
In the previous examples, we used the explicit keyword to prevent the constructor from being used as an implicit user defined conversion from double to complex. Without the explicit keyword, the constructor can be used as a conversion function. Then, only one non-member form of the operator needs to be provided where both arguments are of type complex. Whenever an arithmetic type is supplied as an operand, that type is promoted or converted to a double and then the implicit double to complex conversion is performed. The one overloaded operator- function is then called with two arguments of type complex. The following example illustrates this:
//complex.h (Ex1306)
class complex {
friend complex operator-(const complex &,
const complex &);
public:
complex(double r=0, double i=0) : real(r), imag(i) { }
private:
double real; //real component
double imag; //imaginary component
};
inline complex operator-(const complex &c1,
const complex &c2) {
complex temp;
temp.real = c1.real - c2.real;
temp.imag = c1.imag - c2.imag;
return (temp);
}
//main.cpp (Ex1306)
#include "complex.h"
int main() {
complex c(1.0, 2.0);
double d(3.14159);
c = c - c; //no conversion necessary before call
c = c - d; //double converted to complex before call
c = d - c; //double converted to complex before call
return (0);
}
The use of implicit user defined conversions can help to reduce the number of overloaded operators that must be implemented. However, when such implicit user defined conversions are provided, they can produce subtle changes in the way a program works. For example, if we were to add a complex to double user defined conversion function to our complex class, all uses of operator- containing both complex and arithmetic types would become ambiguous. This is discussed in the next section.
Practical Rule: We recommend that all arithmetic and bitwise binary operators be overloaded as non-member functions.
3.3.4.2 Surprises and Ambiguities
Consider the following implementation of the operator- function as a member of our complex class. In this example, we have provided a constructor that can take a single argument of type double. Such a constructor provides an implicit user defined conversion from a double to a complex. This allows us to use a mixed mode expression as in the following example:
//complex.h (Ex1307)
class complex {
public:
complex(double r=0, double i=0) : real(r), imag(i) { }
complex operator-(const complex &) const;
private:
double real; //real component
double imag; //imaginary component
};
inline complex complex::operator-(const complex &c) const {
complex temp;
temp.real = real - c.real;
temp.imag = imag - c.imag;
return (temp);
}
//main.cpp (Ex1307)
#include "complex.h"
int main() {
complex c(1.0, 2.0);
double d(3.14159);
c = c - d; //double converted to complex
return (0);
}
So far, all is okay. However, if we switch the operands as shown in the next example, we will get a compile error. The first operand must be an object of our class. Even though we have provided a user defined conversion from a double to a complex, it is not applied to the first operand when the operator is implemented as a member function.
//main.cpp (Ex1308)
#include "complex.h"
int main() {
complex c(1.0, 2.0);
double d(3.14159);
c = d - c; //ERROR-first operand must be complex
return (0);
}
Now suppose we add a user defined conversion function converting from a complex to a double. We no longer get a compile error. Instead, our program compiles and executes with no problem. Consider the following example:
//complex.h (Ex1309)
class complex {
public:
complex(double r=0, double i=0) : real(r), imag(i) { }
complex operator-(const complex &) const;
operator double();
private:
double real; //real component
double imag; //imaginary component
};
inline complex complex::operator-(const complex &c) const {
complex temp;
temp.real = real - c.real;
temp.imag = imag - c.imag;
return (temp);
}
inline complex::operator double() {
return real;
}
//main.cpp (Ex1309)
#include "complex.h"
int main() {
complex c(1.0, 2.0);
double d(3.14159);
c = d - c; //uses two implicit user defined conversions
return (0);
}
In order to understand the previous example, we must consider operator overload resolution. Remember from Section 3.3.2 that the candidate operator functions can consist of non-member functions, member functions, and the built-in candidates. In this example, there is only one member candidate and five built-in candidates. The member candidate is not viable because the first operand is not of class type. That leave us with only the built-in candidates as viable functions. Since the first operand is a double and the second operand must be converted from a complex to a double, the best viable function is the subtraction operator taking operands of type double. The residual value of the expression is therefore a double.
The predefined operators expect all operands to be built-in types. Overloaded operators are only invoked if at least one of those operands is a user defined type. However, when user defined conversions to built-in types have been specified, the predefined operators may end up being used even when we use an operator with a user defined type for which an overloaded operator exists!
But wait, we are not done yet! We need to assign the result of our subtraction to our variable c. The type of the residual value is a double. The type of our variable c is complex. Therefore, that double must be converted into a complex before it can be assigned to the variable c. Our simple statement has required two user defined conversions, one to use the built-in subtraction operator and another to use the assignment operator.
Suppose we now use our original expression (c=c-d) after having provided the complex to double conversion function in our class. The following example illustrates what happens:
//main.cpp (Ex1310)
#include "complex.h"
int main() {
complex c(1.0, 2.0);
double d(3.14159);
c = c - d; //ambiguous overloaded function
return (0);
}
It is now ambiguous which overloaded function to use. But wait a minute, we only have one overloaded function! How can it be ambiguous? The answer, of course, is that we must also consider the built-in operator functions. In this case, we have the following two operator function signatures to consider:
operator-(const complex &, const complex &) //member
operator-(double, double) //built-in
Neither signature is best. When comparing our use of the operator- function against our member function, we have an exact match between the first operand with the second operand requiring a user defined conversion. But, when comparing against the built-in operator function, we have an exact match between the second operand with the first operand requiring a user defined conversion. Since there is no operator function for which one argument is better and the remaining arguments at least as good, the result is ambiguous.
This ambiguity only gets worse if we were to make the operator- function a non-member. Both expressions (c=c-d and c=d-c) would become ambiguous because of the complex to double user defined conversion function. Therefore, we recommend avoiding user defined conversion functions from class types to built-in types when overloading operators.
We have seen how implicit user defined conversions can help reduce the number of overloaded operator functions that we have to implement. We have also seen that the use of implicit user defined conversions can work behind the scenes to produce surprising results. If we include user defined conversions, be very careful to understand all of the contexts in which they will be used. If we can't ensure they will always produce the desired results, don't use them.
Practical Rule: User defined type conversions cannot be applied to the first operand when operators are overloaded as member functions. Therefore, whenever we want to take advantage of the first operand being a different type, we must overload that operator as a non-member function.
Practical Rule: Minimize the use of implicit user defined conversions. Avoid implicit conversions from class types to built-in types, especially when used in conjunction with operator overloading.
3.4 Chaining Operators
When overloading operators, not only must we be extra cautious about the types of arguments operators expect, but we must also be especially concerned about the type returned. This is because when we use an overloaded operator, it is often part of a larger expression. We must be able to use the residual value (i.e., the value returned) as the operand of another operator. Remember, our goal should be to allow operators we overload to be used in all of the same contexts as we use built-in types. The following sections discuss when we should return rvalue versus lvalue expressions as a result of an overloaded operator.
3.4.1 Rvalue Expressions
In previous implementations of our complex class, the operator- function returned a complex object by value. That is consistent with the use of the subtraction operator with built-in types: the residual value produced is an rvalue of the type of the operands. Our operator- function does this. It produces an rvalue by returning by value and returns the same type as its operands by returning an object of type complex. That allows our operator- function to be used in larger expressions as shown in the following example:
//main.cpp (Ex1311)
#include "complex.h"
int main() {
complex c1(5.0, 6.0);
complex c2(4.0, 3.0);
complex c3(1.0, 2.0);
complex c;
c = c1 - c2 - c3; //chaining subtraction operators
return (0);
}
Note that subtraction is left associative. The effect we are achieving is the same as chaining explicit operator- function calls together as follows:
c = operator-(operator-(c1, c2), c3); //same thing
3.4.2 Consistent Expressions
Remember, for any class we implement, the compiler provides a default overloaded assignment operator for that class that performs a memberwise assignment of one object into another. Since we can subtract and assign complex numbers, we can write expressions of the following type:
complex c1, c2;
c1 = c1 - c2; //subtract c2 from c1; assign result to c1
Whenever we write an expression like this, we should be able to replace it with a compound assignment operator. Therefore, whenever we overload the arithmetic or bitwise operators, we should also overload the associated compound assignment operators for completeness.
complex c1, c2;
c1 -= c2; //subtract c2 from c1
In order to allow this, we need to overload the subtraction compound assignment operator. This is discussed in the next section.
3.4.3 Lvalue Expressions
The subtraction compound assignment operator subtracts the second operand from the first and produces a residual value that is the type of its first operand. The first operand must be an lvalue expression. The second operand can be an rvalue expression. The result is the same as the first operand and is itself an lvalue expression. This means that we can use the result of the subtraction compound assignment operator as an lvalue. The following example overloads the subtraction compound assignment operator for our complex class.
//complex.h (Ex1312)
class complex {
public:
complex(double r=0, double i=0) : real(r), imag(i) { }
complex &operator-=(const complex &);
private:
double real; //real component
double imag; //imaginary component
};
inline complex &complex::operator-=(const complex &c) {
real -= c.real;
imag -= c.imag;
return (*this); //return change as lvalue
}
//main.cpp (Ex1312)
#include "complex.h"
int main() {
complex c1(6.0, 5.0);
complex c2(4.0, 3.0);
complex c3(1.0, 2.0);
c1 -= c2; //subtract c2 from c1
(c1 -= c2) -= c3; //use lvalue expression (c1-=c2)
return (0);
}
To achieve an lvalue result, we return the result of our subtraction compound assignment operator by reference. This creates an lvalue expression. The lvalue that the expression refers to is the first operand supplied to our overloaded operator-= function. Since that argument is being modified, there is no need to create a temporary complex number to hold the result. Instead, we just modify the first argument directly. Since this operator is overloaded as a member function, the this pointer points to the complex object supplied as the first operand. By dereferencing the this pointer and returning that value, we are returning an lvalue reference to the first operand supplied to our overloaded operator function.
When overloading operators it is important to consider the return type and whether the return type is an rvalue or an lvalue expression. By keeping the return type the same as the operators for the built-in types, we can better ensure that our overloaded operators can be used in the same contexts as we use built-in operators.
3.5 Efficiency Considerations
There are several additional considerations that must be taken in account to improve efficiency when overloading operators. Temporary objects are often created by implicit type conversions or when arguments are returned by value. These temporary objects generally require that implicit copy operations take place. The following sections discuss how to eliminate the creation of unnecessary temporaries and extra copy operations.
3.5.1 Elimination of Temporaries Using Constructors
When an operator and its operands are evaluated, an rvalue is often created. That rvalue is a temporary on the stack that can be used within a larger expression. The lifetime of the temporary is from the time it is created until the end of the statement in which it is used. While the use of temporaries is necessary to protect the original contents of the operator's operands, it does require additional memory and extra (and sometimes redundant) copy operations.
The following example illustrates a common use of temporaries by overloading the negation and subtraction operators. By creating a local variable (temp), we have an object to hold the result of our computations. Since this variable is an automatic variable, the space for it is allcoated at run time on the stack.
//unary non-member function
inline complex operator-(const complex &c) {
complex temp; //temporary object
temp.real = -c.real;
temp.imag = -c.imag;
return (temp); //returned by value
}
//binary non-member function
inline complex operator-(const complex &c1,
const complex &c2) {
complex temp; //temporary object
temp.real = c1.real - c2.real;
temp.imag = c1.imag - c2.imag;
return (temp); //returned by value
}
Examine this example carefully. The value of temp must be copied to the stack by the copy constructor before returning. For large objects, this copy operation can be expensive. We cannot avoid this by returning a reference to temp because its lifetime ends when the function exits.
An alternate approach eliminates the creation of a temporary variable by explicitly using the constructor to create the object as part of the return statement. When this can be done, it saves the cost of copying the object to the stack at return time. A good compiler should perform this optimization automatically. However, by making it explicit we can be sure the savings will be realized. The following example shows how this can be done:
//complex.h (Ex1313)
class complex {
friend complex operator-(const complex &); //unary
friend complex operator-(const complex &,
const complex &); //binary
public:
complex(double r=0, double i=0) : real(r), imag(i) { }
private:
double real; //real component
double imag; //imaginary component
};
//unary non-member function
inline complex operator-(const complex &c) {
return (complex(-c.real, -c.imag));
}
//binary non-member function
inline complex operator-(const complex &c1,
const complex &c2) {
return (complex(c1.real-c2.real, c1.imag-c2.imag));
}
3.5.2 Elimination of Temporaries Using Assignment
When we use the implicitly defined assignment operator to assign one object to another, we may cause a temporary to be formed and an extra copy operation to take place if we are not careful. If the types differ and user defined conversions exist, a temporary object may be created before assignment takes place. Then, that temporary object is what the assignment operation copies.
For example, we can assign an arithmetic type to an object of class complex. This is possible because the complex constructor defines an implicit type conversion from any arithmetic type to a complex type. But, whenever a complex object is needed and an arithmetic type is supplied, a temporary object is created (and complex constructor invoked) with the value of the arithmetic type as the real component and the default of zero as the imaginary component.
In the following example, we assign an integer to a complex object. We have overloaded the assignment operator to display a message when it is called. Except for the message, our overloaded assignment operator does exactly the same thing that the default assignment operator (automatically provided for this class by the compiler) does.
//complex.h (Ex1314)
#include <iostream>
using namespace std;
class complex {
public:
complex(double r=0, double i=0);
complex &operator=(const complex &);
private:
double real; //real component
double imag; //imaginary component
};
inline complex::complex(double r, double i) {
cout <<"constructor called: " <<r <<", " <<i <<endl;
real = r;
imag = i;
}
//Explicit implementation of default assignment operator
inline complex &complex::operator=(const complex &c) {
cout <<"assignment called" <<endl;
real = c.real;
imag = c.imag;
return (*this);
}
//main.cpp (Ex1314)
#include "complex.h"
int main() {
complex c; //default constructor called c(0, 0)
c = 1; //default constructor creates temp c(1, 0)
//and then assignment copies temp to c
return (0);
}
This results in the following output:
constructor called: 0, 0
constructor called: 1, 0
assignment called
While the code to do this is clean and simple, it can be inefficient when working with large objects because of the creation and copying of the temporary object. The constructor is called to create the object and the assignment operator is called to copy the object. If there are many data members and the object is large, such redundant operations can become very expensive.
To solve this problem and make our code more efficient, we can overload the assignment operator to assign objects of the same type, we can also overload the assignment operator to assign an object of a different type. This can be more efficient than using an implicit type conversions as we did in the previous example because no temporary object needs to be created.
The following example demonstrates how we can overload the assignment operator when no dynamic memory is involved. In this example, the assignment operator is overloaded to take an arithmetic type. The constructor has been declared to be explicit to prevent it from being used for an implicit conversion from an arithmetic type to a complex.
//complex.h (Ex1315)
#include <iostream>
using namespace std;
class complex {
public:
complex(double r=0, double i=0);
complex &operator=(double);
private:
double real; //real component
double imag; //imaginary component
};
inline complex::complex(double r, double i) {
cout <<"constructor called: " <<r <<", " <<i <<endl;
real = r;
imag = i;
}
inline complex &complex::operator=(double r) {
cout <<"assignment operator called: " <<r <<endl;
real = r;
imag = 0;
return (*this);
}
//main.cpp (Ex1315)
#include "complex.h"
int main() {
complex c; //default constructor called c(0, 0)
c = 1; //operator=(1) called; no temp created
return (0);
}
This results in the following output:
constructor called: 0, 0
assignment operator called: 1
3.5.3 Elimination of Temporaries Using Compound Assignment
Whenever we overload the arithmetic or bitwise operators, we should also overload the corresponding compound assignment operators. When we do, it is tempting to reuse the overloaded arithmetic or bitwise operators to implement the compound assignment operator. This is shown in the following example.
inline complex &complex::operator-=(double r) {
*this = *this - r; //subtract r from complex object
return (*this); //return modified complex object
}
While this code looks clean and simple, it has serious performance drawbacks. This is because it creates a temporary complex object from the argument, creates a second temporary object as a result of the subtraction operator, and then uses the default copy constructor to copy that temporary back into the original object (*this). If the object was a large object, this simple operation could end up being very expensive!
We should avoid such simple solutions when the costs outweigh the benefits. The following example implements the subtraction compound assignment operator for our complex class much more efficiently.
//complex.h (Ex1316)
class complex {
public:
complex(double r=0, double i=0) : real(r), imag(i) { }
complex &operator-=(double);
private:
double real; //real component
double imag; //imaginary component
};
inline complex &complex::operator-=(double r) {
real -= r;
return (*this);
}
//main.cpp (Ex1316)
#include "complex.h"
int main() {
complex c(1.0, 2.0);
c -= 1.0; //subtract 1 from c
return (0);
}
This section has shown ways to improve the efficiency of overloaded operators. These same concepts can be generally applied to any member function. When implementing members functions that must be efficient, elimination of temporary object and unnecessary copy operations must be at the top of our list of things to check for.
3.6 Overloading the iostream Extraction and Insertion Operators
Overloading the extraction and insertion operators is one of the most common uses of operator overloading. We learned that the >> and << operators are defined to shift bits. In order to use these operators for input and output, the iostream library overloads both operators. The extraction operator (>>) is overloaded for the istream class and the insertion operator (<<) is overloaded for the ostream class. When these operators are used with an object of type istream or ostream (e.g., cin or cout respectively), the overloaded operator in the iostream library is invoked. The iostream library overloads these operators for the built-in data types, but is not equipped to handle new data types that we create. Therefore, in order for extraction and insertion operators to be used with objects of our classes, we must overload these operators ourselves.
We will begin by examining how these operators are overloaded in the iostream library for built-in data types. This is followed by an example of how these same operators can be overloaded for types we create.
3.6.1 Input and Output of Built-in Data Types
When we use the iostream library to read or write built-in data types, we specify the first operand to be an object of class istream (e.g., cin) or ostream (e.g., cout) and the second operand as the type we want to read or write. For example, cin >>int_variable causes an overloaded extraction operator to be invoked and cout <<100 causes an overloaded insertion operator to be invoked. By returning an object of type istream or ostream as a reference, we can chain the operators together. The prototypes for these overloaded operators have the form shown in Figure 3-7.
Figure 3-7: The Syntax for the Extraction and Insertion Operators with Built-in Data Types
3.6.2 Extraction and Insertion Operator Syntax
Overloading the extraction and insertion operators allows user defined types to be read and written in the same manner as built-in data types. This means that whenever we want clients to manipulate user defined types in the same way that they manipulate built-in types, we should consider overloading both the extraction and insertion operators.
The extraction and insertion operators must be overloaded as non-members because the first operand is an object of type istream or ostream and not an object of one of our classes. These operators can be specified as friends whenever they need access to the private information of our objects.
We know from examining how these operators behave on built-in types that extraction will modify the second operand but the insertion operator will not. Therefore, the extraction operation should declare the second operand to be a reference. The insertion operator should specify the second operator to be a constant reference. The return value should be a reference to the object (istream or ostream) that invoked the operator so that chaining can take place. The resulting syntax is illustrated in Figure 3-8
Figure 3-8: The Syntax for Overloading Extraction and Insertion Operators
3.6.3 Example
The following example overloads the extraction and insertion operators for the complex class. When we overload one of these operators we should overload both for consistency.
//complex.h (Ex1316)
#include <iostream>
class complex {
friend std::istream &operator>>(std::istream &,
complex &);
friend std::ostream &operator<<(std::ostream &,
const complex &);
friend complex operator-(const complex &); //unary
friend complex operator-(const complex &,
const complex &); //binary
public:
complex(double r=0, double i=0) : real(r), imag(i) { }
private:
double real; //real component
double imag; //imaginary component
};
//unary non-member function
inline complex operator-(const complex &c) {
complex temp;
temp.real = -c.real;
temp.imag = -c.imag;
return (temp);
}
//binary non-member function
inline complex operator-(const complex &c1,
const complex &c2) {
complex temp;
temp.real = c1.real - c2.real;
temp.imag = c1.imag - c2.imag;
return (temp);
}
//complex.cpp (Ex1316)
#include "complex.h"
#include <iostream>
using namespace std;
//extraction operator for complex
istream &operator>>(istream &i, complex &c) {
i >> c.real; //read real part
i >> c.imag; //read imaginary part
return (i);
}
//insertion operator for complex
ostream &operator<<(ostream &o, const complex &c) {
o <<c.real; //output real part
o <<" "; //output whitespace delimiter
o <<c.imag; //output imaginary part
return (o);
}
//main.cpp (Ex1316)
#include <iostream>
using namespace std;
#include "complex.h"
int main() {
complex c1(4.0, 3.0);
complex c2(1.0, 2.0);
cout <<"Enter two complex numbers:" << endl;
cin >>c1 >>c2;
cout <<"c1=(" <<c1 <<"), c2=(" <<c2 << ")" << endl;
return (0);
}
This results in the following output:
Enter two complex numbers:
5 6
7 8
c1=(5 6), c2=(7 8)
It is tempting when overloading these operators to include prompts and formatting. This should be avoided. Just imagine how awkward our programs would be if every time we read an int or a float the extraction operator would first display a prompt. It would be impossible for the prompt to be meaningful to all possible applications. Plus, what if the input was redirected from a file? Instead, the extraction operator should perform input consistent with the built-in types. When we read any type of data, prompts only occur if we explicitly write one out (e.g., cout <<"Please enter..."). This should remain consistent even with our own types!
When we output any type of data, all we should get is the data. We should be able to write our object out using the insertion operator and then read it back in using the extraction operator. Adding formatting or labels to our output as part of the insertion operator would not allow for this.
Practical Rule: The implementation of our overloaded operators should be as general as possible. It is the client's responsibility to take care of the prompting and formatting.
3.7 Overloading Operations for Dynamic Memory Management
We use the new and delete operators to allocate/deallocate memory for objects. We use the new[] and delete[] operators to allocate/deallocate memory for arrays. While these operators may not be overloaded, C++ has provided functions of the same names that may be overloaded as members of our classes. C++ always provides default global definitions of these functions.
3.7.1 Defining operator new and operator delete Functions
Whenever we use operators to allocate/deallocate memory, C++ causes one of eight different predefined functions to be invoked as shown in Table 3-8. We can provide alternative class specific definitions by overloading these functions as members of a class.
//single objects
void* operator new(size_t);
void* operator new(size_t, std::nothrow_t &);
void delete(void*);
void delete(void*, std::nothrow_t &);
//arrays of objects
void* operator new[](size_t);
void* operator new[](size_t, std::nothrow_t &);
void operator delete[](void*);
void operator delete[](void*, std::nothrow_t &);
Table 3-8: Overloadable Memory Allocation/Deallocation Functions
If a class overloads the operator new, operator delete, operator new[], or operator delete[] functions, then those functions are used rather than the default functions when dynamically allocating and deallocating objects of that type. Overloading the operator new and new[] functions are special cases because no objects exist when the overloaded function is invoked.
When we use the new operator, it calculates the number of bytes needed and in turn invokes the corresponding function to allocate that amount of memory. It calls operator new or operator new[] to allocate memory for the object and then invokes the appropriate constructor(s), as shown in Table 3-3. The operator new function is used if overloaded for that class.
Allocating Dynamic Objects: Result in Calls to:
ptr=new complex operator new(sizeof(complex))
ptr=new complex[5]) operator
new[](sizeof(complex)*5+x)
Table 3-9: Dynamic Objects calling the operator new function
When we allocate memory for an array of objects, the amount of memory needed to support any overhead incurred by an array is represented by x. This is an implementation dependent amount and may vary from one use of new to the next.
When we use the delete operator, it invokes the corresponding function to deallocate the memory pointed to. It invokes the appropriate destructors(s) and then calls operator delete or operator delete[] to deallocate the memory, as shown in Table 3-10. When deallocating memory for an object with the delete operator, the operator delete function is used if overloaded for that class.
Deallocating
Dynamic Objects: Result in Calls to:
delete ptr operator delete(static_cast<void*>(ptr))
delete[] ptr
operatordelete[](static_cast<void*>(ptr))
Table 3-10: Dynamic Objects calling the operator delete function
The client program remains the same regardless of whether an overloaded or default definition of these operator functions are used. To explicitly use the global definitions of these functions, the client may preface the use of new or delete with the scope resolution operator.
Practical Rule: When we overload the operator new function we should overload the corresponding operator delete function. The same applies to the operator new[] and operator delete[] functions. If one is overloaded, the other should also be overloaded.