Introduction to operator overloading

In C we can apply operators on the basic data types but not on the user defined ones e.g. if a, b, and c have been defined as of integer type, then we can have a statement c = a+b;. However, if a, b, and c were of structure type, then the compilation would have failed at the addition statement.

The problem with this is that the user defined type can be really complex for instance

    typedef struct packet_st
    {
        char   *data;  // Data contained in the packet
        time_t timestamp; // time when the packet arrived
    } packet;
For a structure like above how would the compiler know how to add two instances of the structure. Or how can two instances of the structures can be compared to each other.

In C++, the programmer can provide semantics for such operations. The programmer can define functions which would be called when an operator is applied to the instances of user defined type. These functions would be different for different operators and would know how to handle the various operators e.g. a function written for greater than (>) operator for packet structure (defined above) would just compare the timestamp field of the two instances and return true or false.

This mechanism of defining functions in order to apply operators on the user defined types is known as operator overloading and the operator for which the function has been defined is known as being overloaded.

The function that needs to be defined for overloading the operator needs to be in a seciifc format in order for compiler to know that a particular operator is being overloaded. The format for overloading the operator is as follows:

    return_value operator<operator_synbol> (argument_list)
e.g. a function definition to overload the greater than operator would be
    bool packet::operator> (packet p)
    {
        if (difftime(time, p.time) < 0)
            return false;
        else 
            return true;
    }
When the compiler sees the function name as packet::operator> it understands that the operator > for the packet type is being overloaded. So, a statement like if (p1 > p2) could be interpreted as if (p1.operator>(p2)) by the compiler and similarly executed.

In general, for an expression X + Y can be interpreted as X.operator+(Y). The X and Y can be of same or different type.

Overloading operators as member functions

The functions that overload the operators can be defined as the member functions.

    #include <iostream>
    using namespace std;

    class square
    {
        int length;
        public:
        square(int length) { this->length = length ;} //Constructor
        square operator+ (int offset) ; // Increase the side 
        square operator+ (square & s) ; // Add another square

        int getLength() { return length; } 
        int getArea() { return length*length; }
    };

    square square::operator+ (int offset)
    {
        square temp(this->length);
        temp.length += offset;
        return temp;
    }

    square square::operator+ (square &s)
    {
        square temp(this->length);
        temp.length += s.length;
        return temp;
    }


    int main()
    {
        square s1(10), s2(20);
        cout<<s1.getLength()<<"\n";

        s1 = s1 + 10;
        cout<<s1.getLength()<<"\n";

        s1 = s1 + s2;
        cout<<s1.getLength()<<"\n";

        return 0;
    }

    $g++ overload.cc -o overload
    $ ./overload 
    10
    20
    40
In the above code, the operator + has been overloaded for the class square.
    square operator+ (int offset) ; // Increase the side 
    square operator+ (square & s) ; // Add another square
The first version allows us to add interger to instance of class like s1 + 10;.
The second version allows us to add two instances of class like s1 + s2.

In both the functions, the return type is square. This is done so that the result of the + operator can be assigned to some other object such as in s1 = s1 + 10; which is equivalent to s1 = s1.operator(10).
Also, returning an object of square class allows us to chain multiple operators in one statement such as s1 = s1 + s2 + 10;, which is equivalent to s1 = s1.operator+(s2.operator+(10));

When using member function to implement operator overloading one thing to keep in mind is that the first operand of the operator needs to be of the user defined type.

s1 = s1 + 10 ; // Ok -> called as s1.operator+(10)
s1 = 10 + s1 ; // Error -> No function 10.operator(s1)

Maintaining the semantics of an operator

While overloading operators we need to consider the return value for the functions that overload the operators. In the above example we have overloaded the + operator o return an instance of square class. We could have defined the operator to return an bool instead and compiler would not have complained about it.

    class square
    {
        ...
        bool operator+ (int offset) ; // Overload operator +
        ...
    };

    int main()
    {
        ...
        bool status = s1 + s2; // Ok
        ...
    }
The compiler will complain if the return value of the + operator was being treated as square type
        bool operator+ (int offset) ; // Overload operator +
        ...
        s1 = s1 + s2; // Error
        ...

Syntactically returning a bool value from an arithmetic operator is fine, however it distorts the semantics of the + operator. It is programmer's responsibility to maintain the semantics of an operator while overloading the operator. This not only includes the return value of the operator but also the operation performed by the operator as well. We could overload the + operator to perform subtraction internally and the compiler wouldn't care. But the programmer must ensure the correct semantics of an operator is being applied.

Overloading unary operators

Unary operators like -, ++, -- can be overloaded using the member functions as well. Some of the unary operators can be used as prefix and postfix operators (such as ++.) The semantics of overloading a prefix and postfix operator is a little bit different.

Prefix operators

Prefix operators are overloaded same way as the binary operators with the only different that the argument list of the operator function is empty e.g. the function to overload the prefix ++ operator would look like

    square square::operator++()
    {
        this->length++; // Increase the length by one unit
        return *this;
    }
Here, the operator ++ has been overloaded to increase the size of the square by one and return the instance of current square instance.

Following is a complete program

    #include <iostream>
    using namespace std;

    class square
    {
        int length;
        public:
        square(int length) { this->length = length ;} //Constructor

        square operator++ () ; // Overload prefix ++

        square operator+ (int offset) ; // Overload operator +
        square operator+ (square & s) ; // Overload operator +

        int getLength() { return length; } 
        int getArea() { return length*length; }
    };

    square square::operator++ ()
    {
        this->length ++;
        return *this;
    }

    square square::operator+ (int offset)
    {
        square temp(this->length);
        temp.length += offset;
        return temp;
    }

    square square::operator+ (square &s)
    {
        square temp(this->length);
        temp.length += s.length;
        return temp;
    }


    int main()
    {
        square s1(10), s2(20);
        cout<<s1.getLength()<<"\n";

        s1 = s1 + 10;
        cout<<s1.getLength()<<"\n";

        s1 = s1 + s2;
        cout<<s1.getLength()<<"\n";

        ++s1;
        cout<<s1.getLength()<<"\n";

        return 0;
    }

    $ g++ overload.cc -o overload
    $ ./overload 
    10
    20
    40
    41

Postfix operators

In order to overload the postfix operators, we need to provide a dummy integer argument in the agurment list of the operator function e.g. the function to overload the postfix ++ operator would look like

    square square::operator++(int ignore)
    {
        squar etemp ;
        square = *this;
        this->length++; // Increase the length by one unit
        return temp;
    }

Following is a complete program

    #include <iostream>
    using namespace std;

    class square
    {
        int length;
        public:
        square(int length) { this->length = length ;} //Constructor

        square operator++ () ; // Overload prefix ++
        square operator++ (int ignore) ; // Overload postfix ++

        square operator+ (int offset) ; // Overload operator +
        square operator+ (square & s) ; // Overload operator +

        int getLength() { return length; } 
        int getArea() { return length*length; }
    };

    square square::operator++(int ignore)
    {
        square temp = *this;
        this->length++; // Increase the length by one unit
        return temp;
    }

    square square::operator++ ()
    {
        this->length ++;
        return *this;
    }

    square square::operator+ (int offset)
    {
        square temp(this->length);
        temp.length += offset;
        return temp;
    }

    square square::operator+ (square &s)
    {
        square temp(this->length);
        temp.length += s.length;
        return temp;
    }


    int main()
    {
        square s1(10), s2(20);

        s2 = s1++;
        cout<<s1.getLength()<<"\n";
        cout<<s2.getLength()<<"\n";

        return 0;
    }

    $ g++ overload.cc -o overload
    $ ./overload 
    11
    10

Overloading operators as global functions

The functions defined for the overloaded operators need not to be a part of the class. The function can be of a global function as well. With operator functions being member functions, the first operand of the operator was implicit and thus need not to be specified in the argument list of the function, however with global functions we need to specify the first operand of the operator.

    #include <iostream>
    using namespace std;

    class square
    {
        int length;
        public:
        square(int length) { this->length = length ;} //Constructor

        int getLength() { return length; }
        int setLength(int length) { this->length = length; }
    };

    // Overload the + operator for square class
    square operator+ (square &s, int offset) 
    {
        square temp(s.getLength() + offset);
        return temp;
    }

    int main()
    {
        square s1(10);
        cout<<s1.getLength()<<"\n";

        s1 = s1 + 10;
        cout<<s1.getLength()<<"\n";

        return 0;
    }

    $ g++ overload.cc -o overload
    $ ./overload 
    10
    20

Since in global functions we need to specify the first operand explicitly, this means that we can have operators whose first operand is not the instance of the user efined type. So we can have statements which cannot be implemented using memember functions e.g. s1 = 2 + s1 cannot be implemented through member functions because operator + needs to have the first operand as user defined type (i.e. instance of square class in this case) instead of a basic data type. However, we can implement this using global function like this.

    #include <iostream>
    using namespace std;

    class square
    {
        int length;
        public:
        square(int length) { this->length = length ;} //Constructor

        int getLength() { return length; }
        int setLength(int length) { this->length = length; }
    };

    square operator+ (int offset, square &s) 
    {
        square temp(s.getLength() + offset);
        return temp;
    }

    int main()
    {
        square s1(10);
        cout<<s1.getLength()<<"\n";

        s1 = 10 + s1;
        cout<<s1.getLength()<<"\n";

        return 0;
    }

    $ g++ overload.cc -o overload
    $ ./overload 
    10
    20

Overloading operators as friend functions

The global functions that implement the overloaded operators can be declared as friend function as well.
#include <iostream>
using namespace std;

class square
{
    int length;
    public:
    square(int length) { this->length = length ;} //Constructor

    int getLength() { return length; }
    int setLength(int length) { this->length = length; }

    friend square operator+ (int offset, square &s) ;
};

square operator+ (int offset, square &s) 
{
    square temp(s.length + offset);
    return temp;
}

int main()
{
    square s1(10);
    cout<<s1.getLength()<<"\n";

    s1 = 10 + s1;
    cout<<s1.getLength()<<"\n";

    return 0;
}

Now why would you want to decalre the function as friend ? If the class does not have public accessor and modifier functions for every property then you might ant to declare the function as friend, so that the function can access the provate properties as well. In general a friend function denotes a very strong relationship between the function and the class, so define a friend function only if there is need to access the private members.

There's always a catch, and with friend fucntions the catch is that the class declaration needs to be modified. If the programmer doesn't have priviliges or access to the code of the class, then global fuctions have to be used.

Overloading Unary operators

Unary operators can be overloaded using the global functions as well. The only difference with member functions is that global functions will take the operand as their argument while member functions have that arguent as implicit.

Prefix operators

Fucntions overloaidng the prefix operator would have the name of the user-defined type as the first and the only parameter.
    #include <iostream>
    using namespace std;

    class square
    {
        int length;
        public:
        square(int length) { this->length = length ;} //Constructor

        int getLength() { return length; }
        int setLength(int length) { this->length = length; }

    };

    //Overload Prefix increment operator
    square operator++ (square &s) 
    {
        s.setLength(s.getLength() + 1);
        return s;
    }

    int main()
    {
        square s1(10);
        cout<<s1.getLength()<<"\n";

        ++s1;
        cout<<s1.getLength()<<"\n";

        return 0;
    }

    $ g++ overload.cc -o overload
    $ ./overload 
    10
    11

Here, the increment operator (++) has been overloaded in the function square operator++ (square &s) . The instance of class s1 on which the increment operator is applie is passed to the function as argument. The function returns the instance of class so that the resut of increment operator may be assigned to another class object e.g. s1 = ++s1;.

Postix operators

In order to overload the postfix operators, we need to provide a dummy integer argument in the end of the agurment list of the operator function. The first argument is the instance of the user-defined type on which the operator is being applied.
    #include <iostream>
    using namespace std;

    class square
    {
        int length;
        public:
        square(int length) { this->length = length ;} //Constructor

        int getLength() { return length; }
        int setLength(int length) { this->length = length; }

    };

    //Overload Postfix increment operator
    square operator++ (square &s, int ignore) 
    {
        square temp(s.getLength());
        s.setLength(s.getLength() + 1);
        return temp;
    }

    int main()
    {
        square s1(10), s2(20);
        cout<<s1.getLength()<<"\n";

        s2 = s1++;
        cout<<s1.getLength()<<"\n";
        cout<<s2.getLength()<<"\n";

        return 0;
    }

    $ g++ overload.cc -o overload
    $ ./overload 
    10
    11
    10

Resolving overloaded operators

It is possible that same operator has been overloaded in multiple For an expression x@y where x is of type X nd y is of type Y, the compiler looks for the definition of overloaded oeprator @ in the following order:
  1. If X has a defined the operator @, then try to use it.
  2. If base class of X has a defined the operator @, then try to use it.
  3. Look fo the definition of operator @ in the namespaces in the following order:
    1. If the namespace or context in which x@y is being used has the definition of @ then try to use it.
    2. If X is defined in namespace N and N has defined the operator @ then try to use it.
    3. If Y is defined in namespace M and M has defined the operator @ then try to use it.

Exceptions to the rule

The expression x @ y can be interpreted as x.operator@(y) or operator@(x,y). However there are couple of exceptions to this generalization.

Operators that cannot be overloaded

There are couple of operators that cannot be overloaded, which are:

  • :: scope resolution operator
  • . member selection
  • .* member selection through pointer to function
Other than the above, the following operators can be overloaded in C++:
+ - * / % ^ & | ~ !
= < > += -= *= /= %= ^= &=
|= << >> >>= <<= == != <= >= &&
|| ++ -- ->* , -> [] () new new[]
delete delete[]                                                                                                

Member functions only overloading

Though most of the operators can be overloaded using the global/friend functions, the following operators can only be overloaded using the member functions:
  • = assignment operator
  • [] subscript operator
  • () call operator
  • -> member selection operator
Comments