Overloading Arithmetic Operators (+, -, *, /, %)


These operators all have the following things in common:

Overloading Arithmetic Operators as Standalone Functions

The arithmetic operators may always be overloaded as standalone functions. For an operator that will take two objects of your new class as operands, the skeleton of the function definition should always look like this:

ClassName operator symbol(const ClassName& lhs, const ClassName& rhs)
{
    ClassName result;     // Declare a temporary object to hold the result

    // Perform arithmetic with lhs and rhs, storing the result in result

    // Return the temporary result

    return result;
}

In the code skeleton above, items in red will need to be changed by the programmer - plug in the name of your class and the operator symbol you want to overload. Items in black are standard and generally don't change no matter what arithmetic operator is being overloaded or what class the operator is overloaded for.

To see how this works in practice, let's look at a specific example.

Example 1: The * operator overloaded for the Rational class as a standalone function

In this example, the * operator will be used to multiply two Rational objects and get a Rational object as a result. If we take the code above and plug in Rational for ClassName and * for symbol, we'll have the skeleton of the overloaded operator function:

Rational operator*(const Rational& lhs, const Rational& rhs)
{
    Rational result;     // Declare a temporary object to hold the result.

    // Perform arithmetic with lhs and rhs, storing the result in result.

    // Return the temporary result.

    return result;
}

But how do we write the logic to actually multiply the two rational numbers? The answer has less to do with C++ than with mathematics, which defines the product of two rational numbers as:

a   c   ac
― • ― = ――   if b ≠ 0, d ≠ 0
b   d   bd

So, the numerator member of the result should be set to the product of the numerator members of the two operands. The denominator member of the result should be set to the product of the denominator members of the two operands.

As the final step of the arithmetic we will reduce the rational number to its lowest terms. This is standard mathematical practice and it will make implementing some of the other overloaded operators much easier.

Because this function has not been defined as part of the Rational class, it has no direct access to the class's private data members. We will need to use accessor member function calls to obtain the values or change the values of the numerator and denominator. We end up with code that looks like this:

Rational operator*(const Rational& lhs, const Rational& rhs)
{
    Rational result;   // Declare a temporary object to hold the result.

    // Multiply the numerators and denominators of the two rational numbers,
    // storing the result in result.

    result.set_numerator(lhs.get_numerator() * rhs.get_numerator());
    result.set_denominator(lhs.get_denominator() * rhs.get_denominator());

    // Reduce the result to lowest terms:

    // First, find the greatest common divisor for the numerator and 
    // denominator of the result.

    int gcd = find_gcd(result.get_numerator(), result.get_denominator());

    // Then, divide the numerator and denominator by the greatest common divisor.
 
    result.set_numerator(result.get_numerator() / gcd);
    result.set_denominator(result.get_denominator() / gcd);
   
    // Return the temporary result.

    return result;
}

Although the code above will work, notice that twelve (!) accessor member function calls were required to multiply two Rational objects. If we could eliminate the need for these member function calls, it would simplify the code for the function and speed up its execution considerably.

By designating an overloaded operator function to be a friend of the Rational class, we can grant the function direct access to the Rational class’s private data members, eliminating the need to call accessor member functions.

Example 2: The * operator overloaded as a standalone friend function of the Rational class

To designate our overloaded operator function as a friend of the Rational class, we need to include the function's prototype, preceded by the keyword friend, anywhere in the declaration of the Rational class:

class Rational
{
    friend Rational operator*(const Rational&, const Rational&);

private:
    int numerator,
        denominator;

public:
    .
    .
    .
};

The function definition can then be coded like this:

Rational operator*(const Rational& lhs, const Rational& rhs)
{
    Rational result;   // Declare a temporary object to hold the result.

    // Multiply the numerators and denominators of the two rational numbers,
    // storing the result in result.

    result.numerator = lhs.numerator * rhs.numerator;
    result.denominator = lhs.denominator * rhs.denominator;

    // Reduce the result to lowest terms:

    // First, find the greatest common divisor for the numerator and 
    // denominator of the result.

    int gcd = find_gcd(result.numerator, result.denominator);

    // Then, divide the numerator and denominator by the greatest common divisor.
 
    result.numerator /= gcd;
    result.denominator /= gcd;
   
    // Return the temporary result.

    return result;
}

This version of the function is a definite improvement over the version in Example 1.

An overloaded operator implemented as a standalone function should normally be made a friend of the class.

Once we've overloaded this operator as a standalone function, we can use it to multiply two objects of the Rational class:

Rational r1(3, 5);     // Create r1 and set it = 3/5.
Rational r2(2, 3);     // Create r2 and set it = 2/3.
Rational r3;

r3 = r1 * r2;          // Generates the function call r3 = operator*(r1, r2);
                       // Sets r3 = 6/15, which is then reduced to 2/5.

Overloading Arithmetic Operators as Member Functions

Of course, another way of giving a function access to a class's private data is to make the function a member function of the class.

The left operand will be passed implicitly and can be accessed using the this pointer. This means that a member function to overload a binary arithmetic operator will take one argument rather than the usual two.

This also means that the left operand must be an object of our new class in order to overload the operator as a member function of our class. We can code an overloaded operator member function that takes two Rational objects as operands, or an overloaded operator member function that takes a Rational object as the left operand and an int as the right operand. If we want an overloaded operator function that takes an int as the left operand and a Rational object as the right operand, that function can not be implemented as a member function of the Rational class. It will need to be implemented as a standalone friend function instead.

The member function will not change the data members of the object that called it (the left operand), so it should be declared const.

Here's a skeleton of the code for overloading an arithmetic operator as a member function:

ClassName ClassName::operator symbol(const ClassName& rhs) const
{
    ClassName result;    // Declare a temporary object to hold the result.

    // Perform arithmetic with the object pointed to by this and rhs, storing the result in result.

    // Return the temporary result

    return result;
}

Now let's look at a specific example.

Example 3: The * operator overloaded as a member function of the Rational class

Rational Rational::operator*(const Rational& rhs) const
{
    Rational result;   // Declare a temporary object to hold the result.

    // Multiply the numerators and denominators of the two rational numbers,
    // storing the result in result.

    result.numerator = this->numerator * rhs.numerator;
    result.denominator = this->denominator * rhs.denominator;

    // Reduce the result to lowest terms:

    // First, find the greatest common divisor for the numerator and 
    // denominator of the result.

    int gcd = find_gcd(result.numerator, result.denominator);

    // Then, divide the numerator and denominator by the greatest common divisor.
 
    result.numerator /= gcd;
    result.denominator /= gcd;
   
    // Return the temporary result.

    return result;
}

Notice that the code here is extremely similar to the code for Example 2. There's also no difference in how the overloaded operator is used, although the actual call generated by the compiler is different:

Rational r1(3, 5);     // Create r1 and set it = 3/5.
Rational r2(2, 3);     // Create r2 and set it = 2/3.
Rational r3;

r3 = r1 * r2;          // Generates the member function call r3 = r1.operator*(r2);
                       // Sets r3 = 6/15, which is then reduced to 2/5.

Alternate Approaches

It's important to remember that there may be more than one valid way to write the code for an overloaded operator function or member function. For example, there's really no need to explicitly code this-> to access the numerator and denominator of the left operand:

Rational Rational::operator*(const Rational& rhs) const
{
    Rational result;   // Declare a temporary object to hold the result

    // Multiply the numerators and denominators of the two rational numbers,
    // storing the result in result.

    result.numerator = numerator * rhs.numerator;
    result.denominator = denominator * rhs.denominator;

    // Reduce the result to lowest terms:

    // First, find the greatest common divisor for the numerator and 
    // denominator of the result

    int gcd = find_gcd(result.numerator, result.denominator);

    // Then, divide the numerator and denominator by the greatest common divisor
 
    result.numerator /= gcd;
    result.denominator /= gcd;
   
    // Return the temporary result

    return result;
}

Another option is to directly assign one of the operands to the result object and then do the math with that result object and the other operand. That can sometimes allow us write slightly shorter code:

Rational Rational::operator*(const Rational& rhs) const
{
    Rational result = *this;   // Declare a temporary object to hold the result
                               // and initialize it with the left operand.

    // Multiply the numerators and denominators of the two rational numbers,
    // storing the result in result.

    result.numerator *= rhs.numerator;
    result.denominator *= rhs.denominator;

    // Reduce the result to lowest terms:

    // First, find the greatest common divisor for the numerator and 
    // denominator of the result

    int gcd = find_gcd(result.numerator, result.denominator);

    // Then, divide the numerator and denominator by the greatest common divisor.
 
    result.numerator /= gcd;
    result.denominator /= gcd;
   
    // Return the temporary result.

    return result;
}

Depending on what you need to do in your overloaded operator, you might choose to create an empty "default" result, initialize the result with the left operand, or initialize the result with the right operand.

When Member Functions Don't Work

As mentioned above, you can't always overload an arithmetic operator as a member function - it depends on the data type of the left operand. For example, let's say I wanted to be able to use the * operator to find the product of a Rational object and an integer.

If the integer is the right operand, I can overload the operator as a member function, because the member function call that will be generated by the compiler is valid:

Rational r1(3, 5);     // Create r1 and set it = 3/5.
Rational r2;

r2 = r1 * 5;           // Generates the member function call r3 = r1.operator*(5);
                       // Sets r2 = 15/5, which is then reduced to 3/1.

The code for this member function would just be a slight variation of the code to multiply two Rational objects:

Rational Rational::operator*(int rhs) const
{
    Rational result;   // Declare a temporary object to hold the result.

    // Multiply the numerator of the left operand by the integer, storing the 
    // result in result.

    result.numerator = numerator * rhs;
    result.denominator = denominator;

    // Reduce the result to lowest terms:

    // First, find the greatest common divisor for the numerator and 
    // denominator of the result.

    int gcd = find_gcd(result.numerator, result.denominator);

    // Then, divide the numerator and denominator by the greatest common divisor.
 
    result.numerator /= gcd;
    result.denominator /= gcd;
   
    // Return the temporary result.

    return result;
}

However, if the integer is to be the left operand, overloading the operator as a member function would produce an produces an illegal function call:

Rational r1(3, 5);     // Create r1 and set it = 3/5
Rational r2;

r2 = 5 * r1;           // Generates the function call r3 = 5.operator*(r1);
                       // NOT A VALID MEMBER FUNCTION CALL, since 5 is not an
                       // object of the Rational class.

Overloading the operator as a standalone function does produces a valid function call though:

Rational r1(3, 5);     // Create r1 and set it = 3/5.
Rational r2;

r2 = 5 * r1;           // Generates the function call r3 = operator*(5, r1);

Code for the overloaded operator as a standalone function might look like this (assuming that the function is made a friend of the Rational class:

Rational operator*(int lhs, const Rational& rhs)
{
    Rational result;   // Declare a temporary object to hold the result.

    // Multiply the numerator of the right operand by the integer, storing the 
    // result in result.

    result.numerator = lhs * rhs.numerator;
    result.denominator = rhs.denominator;

    // Reduce the result to lowest terms:

    // First, find the greatest common divisor for the numerator and 
    // denominator of the result.

    int gcd = find_gcd(result.numerator, result.denominator);

    // Then, divide the numerator and denominator by the greatest common divisor.
 
    result.numerator /= gcd;
    result.denominator /= gcd;
   
    // Return the temporary result.

    return result;
}

Remember: you can't make an overloaded operator function a member function if its left operand is not an object or is an object of a different class.