Operator Overloading in C++

Operator overloading in C++ allows developers to redefine the behavior of operators for user-defined types.

Operator overloading in C++ is a feature that allows developers to redefine the behavior of operators for user-defined data types (i.e., classes and structures). This means that you can define how operators like +, -, *, [], (), and others behave when applied to objects of a class, just as they do for fundamental types like int and double. Operator overloading enhances the readability of code and enables intuitive use of custom types.

Operators as functions

Consider the following example:

int x { 2 };
int y { 3 };
std::cout << x + y << '\n';

The compiler comes with a built-in version of the plus operator (+) for integer operands.

This function adds integers x and y together and returns an integer result. When you see the expression x + y, you can translate this in your head to a function call operator+(x,y) where operator+ is the name of the function.

Now consider the another example:

double z { 2.0 };
double w { 3.0 };
std::cout << w + z << '\n';

The compiler also comes with a built-in version of the plus operator (+) for double operands. Expression w + z becomes function call operator+(w, z), and function overloading is used to determine that the compiler should be calling the double version of this function instead of the integer version.

Now consider what happens if we try to add two objects of a program-defined class:

Mystring string1 { "Hello, " };
Mystring string2 { "World!" };
std::cout << string1 + string2 << '\n';

What would you expect to happen in this case? The intuitive expected result is that the string “Hello, World!” would be printed on the screen. However, because Mystring is a program-defined type, the compiler does not have a built-in version of the plus operator that it can use for Mystring operands. So in this case, it will give us an error. In order to make it work like we want, we would need to write an overloaded function to tell the compiler how the + operator should work with two operands of type Mystring.

Resolving overloaded operators

When evaluating an expression containing an operator, the compiler uses the following rules:

  • If all of the operands are fundamental data types, the compiler will call a built-in routine if one exists. If one does not exist, the compiler will produce a compiler error.
  • If any of the operands are program-defined types (e.g., one of our classes, or an enum type), the compiler will use the function overload resolution algorithm to see if it can find and overloaded operator that is an unambiguous best match.

What are limitations on operator overloading?

  • First, almost any existing operator in C++ can be overloaded. The exceptions are: conditional (?:), sizeof, scope (::), member selector (.), pointer member selector (*), typeid, and the casting operators.
  • Second, you can only overload the operators that exist. You cannot create new operators or rename existing operators. For example, you could not create an operator** to do exponents.
  • Third, at least one of the operands in an overloaded operator must be user-define type. This means you could overload operator+(int, Mystring), but not operator+(int, double).Because standard library classes are considered to be user-define, this means you could define operator+(double, std::string). However, this is not good idea because a future language standard could define this overload, which could break any programs that used your overload.
  • Fourth, it is not possible to change the number of operands an operator supports.
  • Finally, all operators keep their default precedence and associativity (regardless of what they are used for) and this can not be changed.

Operators that can be overloaded

  • Most operators in C++ can be overloaded. Some common ones include:
    • Arithmetic: +, -, *, /, %
    • Relational: ==, !=, <, >, <=, >=
    • Logical: &&, ||, !
    • Bitwise: &, |, ^, ~, <<, >>
    • Assignment: =, +=, -=, *=, /=
    • Subscript: []
    • Function call: ()
    • Dereference: *, ->
    • Increment/Decrement: ++, --
  • Operators that cannot be overloaded:
    • :: (scope resolution)
    • . (member access operator)
    • .* (pointer-to-member operator)
    • ?: (ternary conditional operator)
    • sizeof (compile-time operator)

Syntax of Operator Overloading

Operator overloading is done by defining a special function with the keyword operator followed by the symbol of the operator. The function can be a member of the class (for binary and unary operators) or a friend function (for situations where the left-hand operand is not an object of the class).

Member function syntax:

class ClassName {
public:
    ReturnType operatorOperatorSymbol(ArgumentList);
};
Example:
class Complex {
public:
    Complex operator+(const Complex& other);
};

Friend function syntax:

Used when the operator needs to access private members but isn't part of the class.

class ClassName {
    friend ReturnType operatorOperatorSymbol(const ClassName& lhs, const ClassName& rhs);
};

Types of Operator Overloading

1️⃣ Unary Operator Overloading:

Unary operators only operate on a single operand. Common unary operators include ++, --, - (negation), and !.

Example: Overloading ++ (prefix increment):

class Counter {
private:
    int count;
public:
    Counter() : count(0) {}

    // Overload the ++ operator (prefix)
    Counter& operator++() {
        ++count;
        return *this;
    }

    int getCount() const {
        return count;
    }
};

int main() {
    Counter c;
    ++c;  // Calls the overloaded prefix ++ operator
    cout << c.getCount();  // Output: 1
}

Example: Overloading ++ (postfix increment) The postfix version can be distinguished by a dummy int parameter.

class Counter {
private:
    int count;
public:
    Counter() : count(0) {}

    // Overload the ++ operator (postfix)
    Counter operator++(int) {
        Counter temp = *this;
        ++count;
        return temp;
    }

    int getCount() const {
        return count;
    }
};

int main() {
    Counter c;
    c++;  // Calls the overloaded postfix ++ operator
    cout << c.getCount();  // Output: 1
}

In the case of the prefix version, the operator modifies the object and then returns a reference to the modified object. For the postfix version, the operator creates a copy of the object (before modification), increments the original object, and returns the unmodified copy.