- 2.1. Our emphasis
- 2.2. The basic goal—a major difference between C++ and Java
- 2.3. Constructors and destructor
- 2.4. Operator overloading in C++
- 2.5. Operator overloading in Java
- 2.6. Flow-control constructs
- 2.7. Manipulating character strings in C++
- 2.8. Canonical class structure
- 2.9. Overcoming macrophobia
- 2.10. Program readability
- 2.11. Error detection and exceptions
2.4. Operator overloading in C++
2.4.1. Member function versus independent function
C++ provides two ways of defining the meaning of an operator applied to one or more objects. We can define an operator function either as a member function or as an independent function. Consider the following code:
Angle theta, phi; . . phi = theta * 2.0;
Unless we’ve defined a meaning for the * operator, the compiler will complain that the operator is not defined for an Angle left operand. To legitimize the above code, we might define an independent function:
Angleoperator* (const Angle Is, const double rs) {Angleresult = Is; result, value *= rs;// (needs friend access) result.normalize(); return result; }
We conventionally use the names Is and rs for the left and right operands of binary operator functions.
Alternatively, we could define * as a member function:
Angle Angle::operator*(const double rs) const {Angleresult = *this; result.value *= rs; result.normalize(); return result; }
Here, the left-side operand is implied: It’s the object for which the function was invoked.
In either case, the compiler simply transforms the normal expression syntax into a function call, so that . . theta * 2.0 . . becomes either
. . operator* (theta, 2.0). . // Operator* defined // as independent
or
. . theta.operator* (2.0). . // Operator* defined // as member
As a general rule, I recommend
the member function, whenever
- the left operand must be an object of the class, or
- the function needs access to private members
the independent function, whenever
- the left-side parameter is not a member of the class, or
- we want to allow either operand to be converted implicitly, by invoking a single-parameter constructor
C++ requires a member function for the assignment operator.
Neither version above takes care of all legitimate multiplications of an Angle by a pure number. The client program might have this coded:
.. 2.0 * theta . .
That won’t match the parameter signature of either the member or the nonmember version. We need two multiplication functions: one of the above and
Angle operator* (const double Is, const Angle rs);
Because multiplication is commutative, implementing the second function is trivial, regardless of whether the other one is a member or an independent function. We simply define it in terms of the other function:
Angle operator* (const double Is, const Angle rs) {return rs * Is;}
Problems and exercises
2.4-1 The last multiplication operator above would be valid for any class as long as multiplication is commutative. Some designers might suggest, therefore, a global function template:
template<class T> T operator* (const T Is, const T rs) {return rs * Is;}
What’s wrong with that suggestion?
2.4.2. Sequence and localization
The last example illustrates defining some overloaded operator functions in terms of others. In order to simplify future maintenance, we should do this whenever it doesn’t compromise efficiency.
Another obvious candidate is the combination of a binary arithmetic operator such as + and the corresponding compound assignment operator +=. Some programmers are irritated when they learn that they have to define both. If we’ve defined +, they argue, shouldn’t the compiler know what += means?
Well, it doesn’t, and tedious as it is, you still have to define both operators. An obvious but somewhat inefficient approach is to define the compound assignment operator as a member function in terms of the simple arithmetic operator:
Money operator+ (const Money rs) const {Money result; result.value = value + rs.value; // (or whatever) return result; } Money&operator+= (const Money rs) {Money result = *this + rs; return *this;}
That works, but as Scott Meyers and others point out, it’s unnecessarily expensive.4 The efficient approach is to define the compound assignment operator first as primitive, and then define the simple binary operator in terms of it:
Money& operator+= (const Money rs) {value += rs.value; return *this;}//Note: no new object Money operator+(const Money rs) const (Money result = *this; return result += rs; }
The latter approach avoids creating a new object in the compound assignment operator function. With that in mind, we advise client programs to prefer compound assignments, especially where the expression contains only one binary operator.
Note that the second version of the simple binary operator function above knows nothing about the object’s internal representation. It could therefore be implemented as a nonmember, non-friend inline function. Some smart compilers may be able to optimize away the resulting object if we rewrite the simple + operator to use an unnamed temporary object by explicitly calling the copy constructor:
Money operator+(const Money rs) const {return Money(*this) += rs; }
Most examples in this book follow Meyers’ recommendation.
Problems and exercises
2.4-2 Both versions of the compound assignment operator += return a reference to the object, while the simple + operator returns an actual object. Are both of those conventions necessary? Why?
2.4-3 Suppose we learn that the project for which we’re developing a class needs only the simple operators and not the compound assignment ones. How would that knowledge alter our strategy in defining binary arithmetic operators for the class?
2.4.3. Increment and decrement operators
In later chapters, we’ll examine when it’s appropriate to overload the increment (++) and decrement (—) operators. Here, we’ll just look at some of the mechanics.
First, we have to distinguish between the prefix version (++k) and the postfix version (k++). C++ recognizes the following artifice:
const ClassName& operator++(); // Prefix version ClassName operator++(int);// Postfix version
The dummy int parameter to the postfix version is never used.
Second, note that the prefix version doesn’t create a new object; it just changes the state of the object for which it’s invoked. The result is a reference, so as to avoid creating a temporary object. We make it const for consistency with C, where the result is not an Lvalue into which the program can store a new value.
Finally, we can always define the postfix version in terms of the prefix version:
ClassNameoperator++(int) {ClassName result = *this; ++(*this); return result; }
2.4.4. In-line versus separately compiled function
In object-oriented programming, many of the methods are much smaller than typical functions in purely procedural programs. An accessor function, for example, often consists only of a return statement. Since conventional subroutine linkage would then account for an unacceptably large percentage of the function’s execution time, C++ needed a construct that provided the modularity of functions without the overhead of subroutine linkage. That construct is the inline function.
You tell the compiler that a function should be generated in-line in either of two ways:
- For any member or independent function, code the inline specifier.
- For a member or friend function, define the function body inside the class definition.
In Java, of course, we fully define all methods within the class definition. We trust the compiler to decide which functions should be generated in-line.
2.4.5. What about exponentiation?
Programmers often complain about C’s lack of the exponentiation operator supported by almost every other procedural programming language, even COBOL. The ability to define operators in C++ may tempt us to try to fill that need, but we’ll be unsuccessful.
Syntactic ambiguities, precedence confusion, or semantic conflicts would result if we were allowed to define, say, x**n or x^n to mean exponentiation.
When would x**p mean x* (*p) ? Should a/b**c mean a/(b**c) or should it mean (a/b) **c? When would a^b have its original Boolean exclusive or meaning? (If you’re skeptical, you can read Stroustrup’s discussion and explanation of this issue.5) So, we’re stuck with using a named function for exponentiation. The C library’s function
double pow(const double x,const double y);
takes care of the most general case of xy, but if you want to go to extra trouble for the common situation in which the exponent is an integer, you can provide an efficient specialized version, such as this recursive function template:6
template<class T> T power(const T x,const int n) {T t; return n == 0 ? 1 // Base cases : n == 1 ? x // (optional) : n < 0 ? 1 / power(x, -n) // Negative power : n%2 == 1? x * power(x,n-1) // Odd power : t = power(x,n/2), t * t; // Even power }
Note that the nested selection (?:) operators don’t require parentheses since they associate left to right. That lets us list the conditions in a column, with the corresponding actions to the right, a rather readable construct once you’re familiar with it. If you’re acquainted with LISP, you’ll recognize this construct as equivalent to the COND function.7
Problems and exercises
2.4-4 The last line of the power function above is illegal in Java. Why? How can the Java programmer change it so that it’s legal and yields the correct result?
2.4-5 Why is the second base case (n==1) optional? What would happen if we removed that line? Why should we leave it in?
2.4-6 How many multiplications will be performed for n=35? Is that optimal?