Lesson #6

Operator Overloading

Overview

In this lesson I will cover the following topics

  1. Operator overloading.

Body

What is operator overloading and why should we care about it?

You have already seen operator overloading in some of my earlier examples. In this lesson, I will describe this important C++ feature in more detail. Operator overloading allows you to define a meaning for the standard operators as they are applied to your own types. In this way, you can make your own types behave much more naturally than would otherwise be the case.

For example, suppose you wanted to create a type complex to hold complex numbers. In fact, the standard library has such a type, but let's ignore that for now. To create your own complex type, you might start by creating a header file named complex.hpp like this

namespace vtsu {

    class complex {
    public:
        // We will fill in some operations shortly.

    private:
        double re, im;
    };
}

Here I'm using the rectangular representation of complex numbers (as a real part and an imaginary part). Alternative representations exist, but those issues aren't my primary concern right now.

Regardless of their internal representation, I would like to make complex numbers as easy and natural to use as possible. In particular, I would like to write things like this

#include "complex.hpp"

int main( )
{
    vtsu::complex Z, X1, X2, X3, X4;

    std::cout << "Enter four complex numbers: ";
    std::cin  >> X1 >> X2 >> X3 >> X4;

    Z = (X1 + X2) / (X1 - X2);
    std::cout << "The result is: " << Z << "\n";
    return 0;
}

Since complex numbers are numbers, I should be able to apply the usual numeric operations on them just the way I can with integers. Also, I should be able to do I/O with complex numbers using inserter and extractor operators. In a language without operator overloading (such as C) all operations would have to be implemented with normal functions. In that case, I would have to write things like:

Z = complex_divide( complex_add( X1, X2 ), complex_subtract( X1, X2 ) );

That is not nearly as nice. It also significantly restricts what I can accomplish with my user-defined type. By making complex numbers seem like "just another number" it is possible to create generic algorithms using a C++ feature called templates (which I will introduce in a later lesson) that apply not only to the built-in types, but also to appropriate user-defined types with the necessary overloaded operators.

Okay, so how does it work?

To overload an operator, you only need to write a function with a special name. For example, here is how I might create an overloaded version of + that knows how to add complex numbers.

complex operator+( const complex &left, const complex &right )
{
  complex result;

  result.re = left.re + right.re;
  result.im = left.im + right.im;
  return result;
}

This function is perfectly ordinary looking except for the funny name: operator+. This special name tells the compiler that this function is to be invoked whenever you try to use + with complex numbers. An expression like Z = X1 + X2 is really Z = operator+( X1, X2 ). In fact, you could even write it that way if you wanted.

Note that in my example above, the operator function is not a method of the class. Thus, in order to have access to the class's private section, it would need to be declared as a friend function in the header file.

class complex {
    friend complex operator+( const complex &left, const complex &right );
public:
    // Public methods.

private:
    double re, im;
};

Friend functions are ordinary functions that have direct access to the private section of the class that declares them as friends. In terms of access control, they are similar to methods. However, unlike a method, a friend function has no implicit object that it is working on. All objects must be passed to the friend in other ways (for example, as parameters).

Notice how I didn't have to put a namespace qualifer on operator+ when I used it? For example, when I just did Z = X1 + X2 how did the compiler know to look in namespace vtsu for the operator+ I'm trying to use? C++ has a special rule that it uses in a case like this. If it can't locate an appropriate operator function any other way, it will search in the namespaces where the types of the operator's arguments are located. In the example above, X1 and X2 are both of type vtsu::complex. Thus, the compiler will know to look in the vtsu namespace for an appropriate operator+. This rule is technically called argument dependent lookup, or ADL, but some people refer to it as "Koenig lookup" (after Andrew Koenig, who first proposed the feature). It is essential for operator overloading to work smoothly with namespaces.

Here are some restrictions that apply when you are overloading operators.

  1. An operator function must have a user-defined type or a reference to a user-defined type as at least one of its parameters. Alternatively, operator functions can be class methods. It is also possible to overload operators that have enumeration types for their parameters.

    This rules out redefining operators that operate exclusively on the built-in types or pointer types. For example, you can't redefine the meaning of + as applied to integers.

  2. You cannot introduce operators. You can only overload operators native to C++, and of those you cannot overload ., ::, ?:, and .*.

  3. You cannot change an operator's precedence or associativity.

On the other hand, operator overloading lifts several restrictions normally present on the operators. In general, operator functions have all the capabilities of ordinary functions.

  1. As long as one parameter is of class (or enumeration) type, the other parameter, if present, can be of any type at all. In fact, you can overload operator functions on parameter types just like ordinary functions.

  2. Operator functions can return any type.

  3. Operator functions can accept non-lvalue arguments and return lvalues (by returning a reference) even if the original operators could not. For example, a statement like

    A + B = C;     // operator+ returns a reference.
    

    might be legal when you use overloaded operators. This example is strange, but the ability for an operator function to return a reference is very important. I will show useful examples of that in later lessons.

  4. You do not need to maintain relationships between operators. For example

    A  = A + 1;  // Probably A = operator+( A, 1 );
    A += 1;      // Probably A.operator+=( 1 );
    ++A;         // Probably A.operator++( );
    

    are all handled with independent operator functions. Of course, to prevent surprises, you should provide the same semantics for all three expressions.

    It may be possible, however, to overload operators so ++A is significantly more efficient than the long-winded A = A + 1. If A were of type int, modern compilers would probably handle all three of the above statements the same way. For an elaborate user-defined type, however, you may want to overload operator+, operator+=, and operator++ separately for efficiency reasons.

In general, you should only overload operators to do what one would naturally expect. Don't overload - to do addition, or * to do subtraction! Operator overloading is a powerful feature, but it is easily abused. Some programmers overload operators much too aggressively. If it is not totally clear that an overloaded operator is appropriate, it probably isn't appropriate.

Method vs non-method vs friend functions.

You can create operator functions that are methods, non-methods, or friends. What are the differences? When an operator function is a class method, one of its arguments is the implicit object against which it is invoked. For example, suppose I wanted to make an overloaded + operator for complex that was a class method. In the header file I would do:

class complex {
public:
    complex operator+( const complex &right );
      // I show only one parameter. The other parameter is the implicit object.

private:
      // etc...
};

To use this operator, I might do:

int main( )
{
    vtsu::complex Z, X1, X2;

    Z = X1 + X2;  // Really Z = X1.operator+( X2 );

    // etc...
}

The left operand is the object against which the method is applied. The right operand is explicit. The asymmetry is a little strange for a case like + since + is inherently symmetrical in its action. However, some operators are asymmetrical anyway (such as +=) and are more natural as methods. Also, unary operators are often more natural as methods. Here is an example of that

namespace vtsu {

    class complex {
    public:
        complex operator-( );
          // Unary minus (used to negate a complex number) takes no
          // parameters when declared as a method. It operates on the
          // implicit object.

    private:
        // etc...
    };
}

int main( )
{
    vtsu::complex Z, X1;

    Z = -X1; // Really Z = X1.operator-( );

    // etc...
}

Symmetric binary operators like + are usually more naturally defined as non-method functions. This is what I did in my earlier examples. However, non-method functions have no special access to the private section of the class. In some cases, this is fine. If the operator function can be written efficiently without such access, then there is no point in giving it such access. In many cases, though, the operator function will need access to the private section. Thus, declaring non-method operator functions to be friends often makes sense.

The value of references and const.

Now is the time for me to describe why references and const are important in C++. Notice how in my examples above I declared my overloaded addition operator to take references to const complex. What if I tried to use pointers to complex numbers instead?

complex operator+( complex *left, complex *right )
{
    complex result;

    result.re = left->re + right->re;
    result.im = left->im + right->im;
    return result;
}

Here I'm using the -> operator to access the members of the objects pointed at by left and right. That is not a problem. The problem is: how would I use this function?

Z = X1 + Z2;

This doesn't work. My operator+ requires pointers.

Z = &X1 + &X2;

This doesn't really work either. I'm trying to add two pointers here, not two vtsu::complex objects. I can't overload operators on just pointers (see the list of restrictions above), so the whole idea of using pointers is ill-founded. Even if it were legal, the requirement of putting & in front of X1 and X2 above is pretty unacceptable.

Of course, I could write my operator function to take its parameters by value like this

complex operator+( complex left, complex right )
{
    complex result;

    result.re = left.re + right.re;
    result.im = left.im + right.im;
    return result;
}

The problem with this is that it requires that my complex objects get copied every time an operator function is called. For a relatively small object like complex (it only contains two doubles as members) that requirement might not be too bad. But for a large object, the time spent copying it would be unacceptable. The only proper solution is to define my operator functions to take references.

Now, why is the const necessary? Remember in an earlier lesson when I said the compiler won't bind a non-const reference argument to a literal number? Here is that example again

void f( int & );
  // Function f takes a reference to a (non-const) int.

int main( )
{
    f( 100 );  // Error!
    return 0;
}

The idea here is that function f might try to modify the object referred to by its parameter and modifying a literal number just doesn't make sense. Now, with that in mind, take another look at this expression involving my complex type

Z = ( X1 + X2 ) / ( X1 - X2 );

What are the operands to the operator/ function? The left operand is the output of the operator+ function, and the right operand is the output of the operator- function. The objects returned by those two functions are anonymous temporary objects. They were not explicitly declared by me, yet they must be set up and managed by the compiler in order for the expression to work. If the operator/ function took references to non-const, the compiler would assume that the function was going to try and modify the anonymous temporaries. That really doesn't make any sense (and it makes it difficult for the compiler to manage those temporaries intelligently). As a result, the compiler would just not allow the expression above to be written. It would be an error.

Thus, to allow elaborate, nested expressions using overloaded operators, you must be sure to declare the parameters of your operator functions as taking references to const. If you leave out the const, you won't be able to use your operators in a natural way.

Don't return references.

Although you normally do want references (to const) as the parameters of your overloaded functions, you normally do not want to return a reference. Instead, you usually want to return a whole object. It is true that returning whole objects might be sluggish when the objects are large, but if you try to do otherwise, subtle errors will creep into your program. Suppose you tried to write operator+ for complex like this:

complex &operator+( const complex &left, const complex &right )
{
    static complex result;
      // This has to be static or it will just vanish when the function returns.

    result.re = left.re + right.re;
    result.im = left.im + right.im;
    return result;
      // This really just returns a reference to result. The result is not copied.
}

This looks great. But look again. What happens when you do:

Z = ( X1 + X2 ) / ( X3 + X4 );

When you first compute X1 + X2 the result is left in a static local object in the operator+ function. Then, when you compute X3 + X4, the new result overwrites the old one. By the time operator/ is called, it will get two references to the latest sum.

In general, to properly evaluate some expressions, objects must occasionally be copied into temporaries. This is just the way it is. If you try to fight that by creating operator functions that return references instead of whole objects, you are flirting with disaster. Don't do it.

I/O operations.

You can use operator overloading to create your own inserter and extractor operators. Here is how to make an operator<< for complex numbers.

std::ostream &operator<<( std::ostream &os, const complex &right )
{
    os << "(" << right.re << ", " << right.im << ")";
    return os;
}

With this function you can say

int main( )
{
    vtc::complex number;

    // Put a value into number.

    std::cout << "The answer is: " << number << "\n";
    return 0;
}

The function defines a new operator<< that takes a reference to an std::ostream on the left and a reference to a (const) complex on the right. It then uses the standard ostream inserter operators to output the components of the complex in some suitable way. Finally, it returns the reference to the ostream. This allows the << operator to be chained so that any further applications of it will be applied to the same ostream (as in the way the "\n" is printed in my example above). An extractor operator can be created similarly. In this way, you can arrange to do I/O on your own types as naturally as with the built-in types.

Notice how this operator function does return a reference. You might wonder what's up with that. Just a few paragraphs earlier, I talked about how returning a reference from an operator function was a bad idea. But this is a different case. Here I'm returning the same reference I've been given in order to pass it on. The real object is located somewhere else. There is no need to create a temporary here. The I/O operators are a bit unusual in this respect as compared to most overloaded operators.

Summary

  1. C++ supports operators overloading. This allows you to define your own functions that will be used whenever someone uses a standard operator with an object of class type. Most operators can be overloaded, but there are some restrictions. At least one of the operator's arguments must be of class type (or enumeration type), new operators can't be introduced, and the association and precedence of existing operators can't be changed. However, operator functions are otherwise completely general; they can do whatever you like.

  2. Overloaded operators can be class methods, ordinary functions, or friend functions. As class methods, their first (left) parameter is always the object they are invoked against. Friend functions are ordinary functions that have access to a class's private section. Often symmetric, binary operators are implemented as friends.

  3. When overloading operators, it is usual to take parameters by reference but to return the result by value. Reference parameters are usually faster because they don't involve copying the operands. However, returning by reference is dangerous because, in general, it isn't possible to allocate the memory for the return value properly.

© Copyright 2023 by Peter Chapin.
Last Revised: August 4, 2023