Operator Overloading

Operator overloading redefines operators so that they 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 simple terms, if using an "operator" is viewed as calling a special function (for example, viewing 1+2 as calling add(1, 2)), and at least one parameter (operand) of this function has type class, struct, or enum, the compiler needs to determine which custom function to call according to the types of the operands.

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

Examples of Some Overloadable Operators

Unary operations: + (unary plus); - (unary minus); ~ (bitwise complement); ++; --; ! (logical NOT); * (dereference); & (address-of); -> (class member access operator), etc.

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

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

Restrictions

Operator overloading has the following restrictions:

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

Implementation

Operator overloading has two cases: overloading as a member function or as a non-member function.

When overloaded as a member function, because an implicit this pointer pointing to the current member is used as a parameter, 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 is the same as the number of operands.

The basic forms are (assuming that the operator to be overloaded is @):

class Example {
  // Example of a member function
  return_type operator@(parameters_except_this) { /* ... */ }
};

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

Several examples of overloaded operators follow.

Basic Arithmetic Operators

The following defines a two-dimensional vector structure Vector2D and implements overloads for the corresponding addition and inner product.

Example of Overloading Arithmetic Operators
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 that the return type does not have to be 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 types: prefix (++a) and postfix (a++). To distinguish prefix and postfix operators, an empty parameter of type int needs to be added when overloading a postfix operation.

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

Example of Separately Overloading Prefix and Postfix Increment Operators
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 among built-in increment and decrement operators, the prefix operator returns a reference, while the postfix operator returns a value. Although an overloaded operator does not have to follow this restriction, semantically it is still expected that the return type of an overloaded operator is consistent with that of the built-in operator.

For type T, the typical definitions of overloaded increment operators are as follows:

Overloaded definition (using ++ as an example)Member functionNon-member function
PrefixT& T::operator++();T& operator++(T& a);
PostfixT 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 structure with an overloaded () operator as a custom comparison function to STL containers such as priority queues.

Here is an example: given the names and scores of nn students, sort them in descending order of score; students with equal scores are sorted in ascending lexicographical order of name; output the name and score of the person with the highest rank.

The following defines a comparison structure and implements a custom ordering method for a priority queue.

Example of Overloading the Function Call Operator
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 that the template parameter is the structure name, not an instance
priority_queue<student, vector<student>, cmp> pq;

Comparison Operators

The < operator is needed in std::sort and some STL containers. When using a custom type, we need to overload it manually.

Here is an example implementing the same functionality as in the previous section.

Example of Overloading Comparison Operators
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 complete expression is:
    // this->scorescore==a.score&&this->name>a.name);
  }
};

priority_queue<student> pq;

The code above overloads the less-than operator as a member function. Of course, it can also be overloaded as a non-member function.

Overloading as a Non-member Function
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 exists, the other five comparison operators can also be implemented easily.

/* 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) { /* Here overload the less-than operator */ }
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); }
Three-way Comparison Operators in C++20

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

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

The order of default comparison is to compare members one by one in the order in which the member variables are declared.4

A custom three-way comparison can also be used. In this case, one of the comparison category types (std::strong_ordering, std::weak_ordering, or std::partial_ordering) must be selected, or an object must be returned 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, see Comparison operators #Three-way comparison - cppreference.

References and notes:

Footnotes

  1. Operator overloading - cppreference ↩ ↩2

  2. User-defined literals - cppreference ↩

  3. Comparison operators #Three-way comparison - cppreference ↩

  4. Default comparisons - cppreference ↩

This content comes from OI Wiki: Operator Overloading. See the original for authors and revision history. AIOJ has adapted the formatting, in-site links, and resource paths.

Content is shared under CC BY-SA 4.0 and SATA license; additional terms are governed by the original notice. Thanks to the OI Wiki authors and contributors; per the SATA terms, please visit the OI Wiki repository and star it to show your support.