Introduction to Operator Overloading

In a previous lesson on function overloading, you learned that multiple functions can share the same name as long as they have unique function signatures. This allows you to create variations of a function for different data types without inventing unique names for each.

C++ implements operators as functions. By applying function overloading to operator functions, you can define custom versions that work with your own classes. This is called operator overloading.

Operators as Functions

Consider this expression:

int a{10};
int b{4};
std::cout << a + b << '\n';

The compiler has a built-in version of the + operator for integer operands. When you write a + b, think of it as calling operator+(a, b).

The same applies to doubles:

double x{3.5};
double y{1.2};
std::cout << x + y << '\n';

The compiler provides a built-in + for doubles too. Function overloading determines whether the integer or double version is called based on operand types.

But what happens with a user-defined class?

Currency usd{50, 25}; // 50 dollars, 25 cents
Currency eur{30, 75}; // 30 dollars, 75 cents
std::cout << usd + eur << '\n';

Intuitively, we'd expect 81 dollars and 0 cents. However, since Currency is user-defined, the compiler has no built-in + operator for it, causing a compile error. To make this work, we must write an overloaded operator function that tells the compiler how to add two Currency objects.

Resolving Overloaded Operators

When the compiler evaluates an expression with an operator:

  • If all operands are fundamental types, the compiler uses a built-in routine if one exists. Otherwise, compilation fails.
  • If any operand is a user-defined type (classes or enums), the compiler uses function overload resolution to find an appropriate overloaded operator. This may involve implicit conversions. If no match or an ambiguous match is found, compilation fails.

Limitations on Operator Overloading

First, most C++ operators can be overloaded. The exceptions include:

  • Conditional (?:)
  • sizeof
  • Scope (::)
  • Member selector (.)
  • Pointer member selector (.*)
  • typeid
  • Casting operators

Second, you can only overload existing operators. Creating new operators or renaming existing ones is not allowed. For example, there's no operator** for exponentiation.

Third, at least one operand must be a user-defined type. You could overload operator+(int, Currency), but not operator+(int, double).

Since standard library classes are user-defined, you could define operator+(double, std::string). However, this is inadvisable - a future language standard might define this overload, breaking your code.

Best Practice
An overloaded operator should operate on at least one program-defined type (either as a parameter or the implicit object).

Fourth, you cannot change the number of operands an operator supports.

Finally, all operators keep their default precedence and associativity, which cannot be changed.

Some programmers attempt to overload the bitwise XOR operator (^) for exponentiation. However, in C++, operator^ has lower precedence than arithmetic operators. In mathematics, exponentiation has higher precedence:

  • Mathematical: 2 + 5^2 = 2 + 25 = 27
  • C++: 2 + 5 ^ 2 = (2 + 5) ^ 2 = 7 ^ 2 = 49

You'd need to parenthesize every exponent expression: 2 + (5 ^ 2), which is unintuitive and error-prone.

Best Practice
Use operators only in ways analogous to their original intent. If the meaning isn't clear and intuitive, use a named function instead.

Operator names don't convey purpose, so it's not always obvious what they do. For example, operator+ might concatenate strings, but what would operator- do? Split them? Remove characters? The meaning is unclear.

Best Practice
If an overloaded operator's meaning is not clear and intuitive, use a named function instead.

Finally, overloaded operators should return values consistently with the original operators. Operators that don't modify operands (e.g., arithmetic operators) should generally return by value. Operators that modify their leftmost operand (e.g., pre-increment, assignment operators) should generally return the leftmost operand by reference.

Best Practice
Operators that don't modify operands (e.g., arithmetic operators) should generally return by value.

Operators that modify their leftmost operand (e.g., pre-increment, assignment operators) should generally return the leftmost operand by reference.

Within these constraints, operator overloading is incredibly useful! You can overload + to add vectors, == to compare objects, << to print custom classes, and more. This makes working with classes intuitive and natural.

In upcoming lessons, we'll explore overloading different kinds of operators in detail.

Summary

Operator overloading: Using function overloading on operator functions to define custom versions that work with user-defined types. Operators are implemented as functions, so a + b is essentially operator+(a, b).

Overload resolution: When all operands are fundamental types, the compiler uses built-in routines. When any operand is user-defined, the compiler uses function overload resolution to find an appropriate overloaded operator, potentially performing implicit conversions.

Limitations on operator overloading: Cannot overload conditional (?:), sizeof, scope (::), member selector (.), pointer member selector (.*), typeid, or casting operators. Cannot create new operators or rename existing ones. At least one operand must be user-defined (preferably program-defined, not standard library). Cannot change the number of operands. Cannot change precedence or associativity.

Best practices: Use operators only in ways analogous to their original intent. If meaning isn't clear and intuitive, use a named function instead. Operators that don't modify operands should return by value. Operators that modify their leftmost operand should return that operand by reference.

Operator overloading makes custom classes feel natural and intuitive to use, enabling you to write code like total = price1 + price2 or std::cout << myObject instead of awkward function calls like total = add(price1, price2) or myObject.print(). The key is using operators in ways that users will find obvious and expected.