Operator Overloading in C++

Introduction

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.

Operator Overloading using Member Functions

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.
  1. Copy the directory ~csci208/labs/overloading and set the copy as your working directory.
  2. Edit the file rational.cxx to implement member functions that support the operations shown in the table, where r1 and r2 are rational numbers--that is, instances of the class Rational:
    r1 == r2

    operator==

    The equality operator member function requires one operand, a rational number, and returns a Boolean value of true if the two rational numbers are equal, false otherwise.
    r1 < r2

    operator<

    The less than operator member function requires one operand, a rational number, and returns a Boolean value of true if the receiver is numerically less than the argument, false otherwise. [Note: if r1 = a/b and r2 = c/d, then r1 < r2 if and only if (a*d) < (c*b).]
  3. make the program and run it until it passes successfully, making changes as necessary to the file rational.cxx.

Operator Overloading using Functions

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:

  1. Operator overloading is a very nice feature of C++ from the perspective of support for abstraction. At the same time, it can be very frustrating to learn how to use it. Since the textbook, as well as many designers of C++ classes, use operator overloading quite a bit, you should be familiar with the concept, syntax, and semantics. However, do not feel obligated to define overloaded operators. Postpone their use until after you have mastered other basic concepts of the language.
  2. Operators are not the only things that can be overloaded in a C++ program. Functions and member functions can also be overloaded--that is, two or more member functions in the same class can have the same name and two or more functions can have the same name. Thus, for example, two functions called abs can be defined to compute the absolute value of a number:
       
        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.

  3. Overloading of member functions is particularly useful. When more than one constructor is defined in a class, overloading occurs. [The signature of the constructors is used to determine which to use.] A useful member function for the Rational class would support addition of integers, meaning the operator+ operation would be overloaded, adding into the class interface the declaration:
       
        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.
  1. Modify the file rational.h to declare a prototype for a function to allow an integer and a rational number to be compared for equality and less than. These functions have the prototypes
        Boolean operator==(int i, const Rational& r);
        Boolean operator<(int i, const Rational& r);

    These declarations are made "outside" the class declaration for Rational.

  2. Modify the file rational.cxx to define these functions.
  3. Modify the function main() to add the following lines
        Rational q1(1, 2);
        Rational q2(2, 2);
        if ( 1 == q2 ) cout << "[1] PASSED: operator==" << endl;
        if ( ! (2 == q1) ) cout << "[2] PASSED: operator==" << endl;
        if ( 1 < (1 + r1) )  cout << "[3] PASSED: operator<" << endl;
        if ( ! (1 < r1) )  cout << "[4] PASSED: operator<" << endl;
       
  4. make the program and repeat these steps until the program compiles, links, and executes correctly--that is, you see the four PASSED messages printed.
  5. Print the files rational.h and  rational.cxx containing your changes on the lab printer using the shell command [The pipe character is Shift+'\' on most keyboards. Type the names of the team members in place of your names. ]:

    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