Skip to content

Operator Overloading

Operator overloading is the redefinition of operators to support operations on specific data types. Operator overloading is a special case of function overloading.

When an operator appears in an expression, and at least one of its operands has a class or enumeration type, overload resolution is used to determine which user-defined function satisfying the corresponding declaration should be called.1

In simpler terms, if using an "operator" is considered as calling a special function (e.g., treating 1+2 as calling add(1, 2)), and at least one of the function's parameters (operands) is of type class, struct, or enum, the compiler needs to decide which custom function to call based on the operand types.

In C++, we can overload almost all available operators.

List of Some Overloadable Operators

Unary operators: + (positive sign); - (negative sign); ~ (bitwise NOT); ++; --; ! (logical NOT); * (dereference); & (address-of); -> (member access) etc.

Binary operators: +; -; & (bitwise AND); [] (subscript); ==; = (assignment) etc.

Others: () (function call); "" (user-defined literals1, since C++11); new (memory allocation); , (comma operator); <=> (three-way comparison2, since C++20) etc.

Restrictions

The following restrictions apply to operator overloading:

  • Only existing operators can be overloaded; new operators cannot be defined.
  • The following operators cannot be overloaded: :: (scope resolution), . (member access), .* (member access through pointer to member), ?: (ternary conditional).
  • The precedence, number of operands, and associativity of an overloaded operator cannot be changed.
  • Overloading && (logical AND) and || (logical OR) loses short-circuit evaluation.

Implementation

Operator overloading can be implemented in two ways: as a member function or as a non-member function.

When overloaded as a member function, there is an implicit this pointer pointing to the current object as a parameter, so the number of function parameters is one less than the number of operands.

When overloaded as a non-member function, the number of function parameters equals the number of operands.

The basic format is as follows (assuming the operator to be overloaded is @):

1
2
3
4
5
6
7
class Example {
  // Example as a member function
  return_type operator@(parameters_except_this) { /* ... */ }
};

// Example as a non-member function
return_type operator@(all_operands_as_parameters) { /* ... */ }

Several examples of operator overloading are given below.

Basic Arithmetic Operators

The following defines a 2D vector struct Vector2D and overloads the addition and dot product operators.

Example of Overloading Arithmetic Operators
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
struct Vector2D {
  double x, y;

  Vector2D(double a = 0, double b = 0) : x(a), y(b) {}

  Vector2D operator+(Vector2D v) const { return Vector2D(x + v.x, y + v.y); }

  // Note: the return type need not be of this class
  double operator*(Vector2D v) const { return x * v.x + y * v.y; }
};

Increment and Decrement Operators

Increment and decrement operators are divided into two categories: prefix (++a) and postfix (a++). To distinguish between them, when overloading the postfix operator, an extra dummy parameter of type int must be added.

Prefix increment can be thought of as calling operator++(a) or a.operator++(), while postfix increment can be thought of as calling operator++(a, 0) or a.operator++(0).

Example Overloading Prefix and Postfix Increment Operators Separately
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
struct MyInt {
  int x;

  // Prefix, corresponding to ++a
  MyInt &operator++() {
    x++;
    return *this;
  }

  // Postfix, corresponding to a++
  MyInt operator++(int) {
    MyInt tmp;
    tmp.x = x;
    x++;
    return tmp;
  }
};

Another point is that for built-in increment/decrement operators, the prefix operator returns a reference, while the postfix operator returns a value. Although overloaded operators are not required to follow this restriction, semantically it is still expected that the return types of overloaded operators match those of the built-in operators.

For a type T, typical definitions for overloading the increment operator are as follows:

Overload Definition (using ++ as example) Member Function Non-member Function
Prefix T& T::operator++(); T& operator++(T& a);
Postfix T T::operator++(int); T operator++(T& a, int);

Function Call Operator

The function call operator () can only be overloaded as a member function. By overloading the () operator for a class, objects of that class can be called like functions.

A common application of overloading the () operator is to pass a struct with an overloaded () operator as a custom comparison function into STL containers like priority queues.

Here is an example: Given the names and scores of \(n\) students, sort them in descending order by score; for equal scores, sort in ascending order by name lexicographically, and output the name and score of the top-ranked student.

The following defines a comparison struct to implement a custom sorting method for a priority queue.

Example of Overloading the Function Call Operator
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
struct student {
  string name;
  int score;
};

struct cmp {
  bool operator()(const student& a, const student& b) const {
    return a.score < b.score || (a.score == b.score && a.name > b.name);
  }
};

// Note: The template parameter passed is the struct name, not an instance
priority_queue<student, vector<student>, cmp> pq;

Comparison Operators

In std::sort and some STL containers, the < operator is required. When using custom types, we need to overload it manually.

The following example achieves the same functionality as the previous section.

Example of Overloading Comparison Operators
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
struct student {
  string name;
  int score;

  // Overload the < operator
  bool operator<(const student& a) const {
    return score < a.score || (score == a.score && name > a.name);
    // The this pointer is omitted above; the full expression is:
    // this->score<a.score||(this->score==a.score&&this->name>a.name);
  }
};

priority_queue<student> pq;

The code above overloads the less-than operator as a member function, but overloading it as a non-member function is also possible.

Overloading as a Non-member Function
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
struct student {
  string name;
  int score;
};

bool operator<(const student& a, const student& b) {
  return a.score < b.score || (a.score == b.score && a.name > b.name);
}

priority_queue<student> pq;

In fact, once the < operator is defined, the other five comparison operators can be easily implemented.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
/* clang-format off */

// The following implementations all overload the less-than operator as a non-member function

bool operator<(const T& lhs, const T& rhs) { /* Overload the less-than operator here */ }
bool operator>(const T& lhs, const T& rhs) { return rhs < lhs; }
bool operator<=(const T& lhs, const T& rhs) { return !(lhs > rhs); }
bool operator>=(const T& lhs, const T& rhs) { return !(lhs < rhs); }
bool operator==(const T& lhs, const T& rhs) { return !(lhs < rhs) && !(lhs > rhs); }
bool operator!=(const T& lhs, const T& rhs) { return !(lhs == rhs); }

About the Three-Way Comparison Operator in C++20

If using C++20 or later, we can directly use the default three-way comparison operator to simplify the code.3

1
auto operator<=>(const T &lhs, const T &rhs) = default;

The default comparison compares member variables in the order they are declared.4

Custom three-way comparison is also possible. In this case, you must choose the underlying ordering relation (std::strong_ordering, std::weak_ordering, or std::partial_ordering), or return an object such that:

  • If a < b, then (a <=> b) < 0;
  • If a > b, then (a <=> b) > 0;
  • If a and b are equal or equivalent, then (a <=> b) == 0.

For specific implementation details, refer to Comparison operators #Three-way comparison - cppreference.

References and Notes: