C++ provides operator overloading, a feature that many other object-oriented programming languages do not provide. Operator overloading allows member function calls (and even "regular" function calls) to be written using infix operator notation. Consider, for example, the rational class defined in our textbook [pp. 313 ff.]. Among the operations on rational numbers are addition and multiplication. In terms of abstraction, it would be nice to be able to manipulate rational numbers in a program much the same way they are manipulated by mathematicians-for example, if p and q are rational numbers, then to add them together using the expression p + q. In what we have discussed about C++ in class so far, the best that can be done is something such as:
| p.plus(q) |
Operator overloading permits a member function to be defined such that a program can utilize more natural notations, allowing the writing of p +q in a program. The availability of overloading is another step toward the use of abstraction in program design. Once operators for addition, subtraction, multiplication, and division of rational numbers are defined, a programmer can view rational numbers as being "built-in" to C++, allowing algorithms involving rational numbers to be expressed quite naturally. The term operator overloading is used because now, for example, the operator + can appear in a variety of contexts--between two int-type variables, two float-type variables, and now two rational-type variables. Such an operator can be defined for any class, if it makes sense to do so.
C++ supports overloading for just about every operator defined in the language:
| + | - | * | / | % | ^ | & | | | ~ | ! |
| = | < | > | += | -= | *= | /= | %= | ^= | &= |
| |= | << | >> | >>= | <<= | == | != | <= | >= | && |
| || | ++ | -- | ->* | , | -> | [] | () | new | delete |
Each operator has requirements with respect to its parameters since some are unary--that is, involve one operand, such as as the operator !--and some are binary--that is, involve two operands, such as the operator +.
| Don't get carried away with the use of operator overloading. One could, for example, define a + operator for stacks to mean "push" or a suffix -- operator to mean "pop an return what was on top." However, such operator notation is unconventional among people who work with stacks and the use of such an operator is much more confusing than operations called push, pop, and top. |
Here is the rational class from the textbook,expressed using the coding standards and conventions we are following and making a few improvements--such as passing parameters by reference instead of by value:
| class Rational { |
| public: |
| Rational(); |
| Rational(int numerator, int denominator); |
| int numerator() const; |
| int denominator() const; |
| Rational operator+(const Rational& r) const; |
| Rational operator-(const Rational& r) const; |
| Rational operator*(const Rational& r) const; |
| Rational operator/(const Rational& r) const; |
| Rational& operator=(const Rational& r); |
| private: |
| int gcd(int a, int b); // compute greatest common divisor of _a_ and _b_ |
| void reduce(); // reduce my numerator and denominator |
| // to lowest terms |
| int myNum; // my numerator |
| int myDenom; // my denominator |
| }; |
The operator+ and operator= operations in the public interface for Rational supports writing code such as:
| Rational r1(1, 2); |
| Rational r2(1, 4); |
| Rational r3(); |
| r3 = r1 + r2; // Uses operator+ and then operator= for assignment |
The expression r1 + r2 is interpreted as a request to the object named by r1 to perform the addition (operator+) operation, passing r2 as the argument. Thus, this is a member function call with r1 as the receiver and r2 as the argument. The assignment is also a member function call--a request for r3 to perform operator= using the rational number returned by the addition operation as the argument. [The operator+ member function returns an object as a value!] C++ also allows the more conventional member function call syntax, although it looks rather awkward:
| Rational r1(1, 2); |
| Rational r2(1, 4); |
| Rational r3(); |
| r3.operator=( r1.operator+(r2) ); |
The addition and assignment operations can be implemented as follows:
| Rational Rational::operator+(const Rational& r) const { |
| int num = (myNum * r.myDenom) + (r.myNum * myDenom); |
| int denom = (myDenom * r.myDenom); |
| Rational sum(num, denom); |
| return sum; |
| } |
| Rational& Rational::operator=(const Rational& r) { |
| myNum = r.myNum; // copy r's numerator to mine |
| myDenom = r.myDenom; // copy r's denominator to mine |
| return *this; // return myself! |
| } |
The expression returned in the last line of Rational::operator= evaluates to the object performing the assignment operation. Thus, the object returned is the object that received the request to do an assignment. [This might seem a bit odd in that this should have been a void function. However, C++ treats assignment as an expression that has a value, so a non-void return type must be specified. This treatment of assignment as an expression is what allows you to introduce bugs into your programs such as
| if ( r1 = r2 ) // should be r1 == r2 |
[Now you can make the same mistake with objects!]
Something curious occurs in the code for the member functions operator+ and operator= functions. Note the references to the private data members of the object r. This seems to violate the data hiding enforced for the private part of an object. In C++, an object's private data members are accessible to another object of the same class. The rationale is fairly simple:
If I am an object in the class Rational and another object I'm working with is an object in the same class, then it's okay for me to access that other object's private data members and member functions since they are exactly the same as what I have. Thus, data hiding is not really violated.
Do you buy this rationale?
Exercise |
||||
Work in teams of two.
|
C++ also supports overloading of functions--that is, non-member functions. This is often useful when there is a need to overload an operator when the left operand corresponds to a value of one of the predefined types--for example, we'd like to be able to write the expression
| Rational r1(1, 2); |
| Rational r3; |
| int n = 100; |
| r3 = n + r1; |
to add 100 to a rational number. However, since n is not an object--in particular an instance of the class Rational--then there is no member function that can be invoked. However, a function (not a member function) can be defined that will do just what we would like to have happen:
| Rational operator+(int i, const rational& r) { |
| Rational sum( i * r.denominator() + r.numerator(), r.denominator() ); |
| return sum; |
| } |
The expression
| n + r1 |
is then interpreted as a call to this function. The expression could also be written as the rather unwieldy and unappealing function call
| operator+(n, r1) |
This is an invocation of the function to add an integer and a rational, not an invocation of a member function. Note the member function operator+ has one argument while the [standalone] function has two arguments. The member function uses the receiver as one of the operands and the argument as the other. The function needs two arguments to specify the operands.
Notes:
| int abs(int i); |
| float abs(float x); |
Every function has a signature, determined by the name of the function, the number and types of its formal parameters, and whether or not it is declared const. [The return type of the function is not part of the signature.] Since the signatures of the two abs functions are different--even though they have the same function name--then these functions can co-exist in the same program. Whenever a call abs(b) is encountered during compilation, the compiler considers the type of b in determining which of the overloaded functions to call. Thus, the call abs(-7) is considered a call to the first function listed above since -7 is of type int since the single int argument matches the argument called for in that function's signature. The call abs(z), where z is declared as a variable of type float, is considered a call to the second function listed above since the single float argument matches the argument called for in that function's signature.
In the same program, the following functions could also defined since their signatures are unique:
| Boolean abs(int i, int j); |
| Boolean abs(float x, float y); |
| Rational abs(const Rational& r); |
| void abs() |
Of course, if all functions have the same name, a program becomes very difficult to read! However, the opportunity to overload function names in programs is very useful at times and should not be avoided if their use supports abstraction. For example, being able to use the function abs to compute the absolute value of either a float or an int value is more natural than having to use, say, iabs to compute the absolute value of an int value and fabs to compute the absolute value of a float value.
| Rational operator+(int i) const; |
With the addition of this member function, integers and rationals can be added together in any order.
Exercise |
|||||||||
Continue working in teams of two.
These declarations are made "outside" the class declaration for Rational.
pr -f -e4 -h "your names" rational.h rational.cxx | lpr -Pthur301 This command runs the pr command, which places a header on each page of output, and then routes its output to the lab printer using the lpr command. |
Last updated Wednesday, July 04, 2001 12:25 PM by Will Thacker