What are the basic rules and idioms for operator overloading?

Operator overloading is a powerful feature in C++ that allows you to redefine the behavior of an operator for user-defined types. It enables you to write code that is more expressive and intuitive, making your classes behave like built-in types. In this article, we will explore the basic rules and idioms for operator overloading in C++.

The General Syntax of Operator Overloading in C++

Operator overloading in C++ follows a specific syntax. To overload an operator, you need to define a function with the keyword "operator" followed by the operator you want to overload. The function can be a member function or a non-member function, depending on the context in which you want to use the operator. Let's take a look at the general syntax for operator overloading:

return_type operatorOPERATOR(parameters) {
    // Implementation of the operator
}

The Three Basic Rules of Operator Overloading in C++

  1. Overloaded operators should maintain the semantics associated with the original operator as much as possible.
  2. Overloaded operators should typically be implemented as non-member functions for symmetrical operations.
  3. Overloaded operators should be implemented as member functions for operations that modify the state of the object.

The Decision between Member and Non-member

When deciding whether to implement an overloaded operator as a member function or a non-member function, there are a few factors to consider:

  • If the operator requires access to the private members of the class, it should be implemented as a member function.
  • If the operator does not modify the state of the object and can be implemented using only the public interface of the class, it can be implemented as a non-member function.
  • If the operator needs to be symmetrical (i.e., when the order of operands does not matter), it is typically implemented as a non-member function.

Common Operators to Overload

There are several common operators that can be overloaded in C++. Let's take a look at some of them:

Assignment Operator

The assignment operator (=) is used to assign the value of one object to another. It can be overloaded to provide custom behavior when assigning objects of your class type. Here's an example:

MyClass& operator=(const MyClass& other) {
    // Implementation of the assignment operator
    return *this;
}

Stream Insertion and Extraction

The stream insertion (<<) and extraction (>>) operators are used for input and output operations. They can be overloaded to enable streaming of objects of your class type. Here's an example:

friend std::ostream& operator<<(std::ostream& os, const MyClass& obj) {
    // Implementation of the stream insertion operator
    return os;
}

friend std::istream& operator>>(std::istream& is, MyClass& obj) {
    // Implementation of the stream extraction operator
    return is;
}

Function Call Operator

The function call operator () allows objects of your class type to be called like functions. It can be overloaded to make the objects behave like function objects or functors. Here's an example:

return_type operator()(parameters) {
    // Implementation of the function call operator
}

Logical Operators

The logical operators (&&, ||, !) are used for logical operations. They can be overloaded to provide custom behavior for logical operations involving objects of your class type. Here's an example:

bool operator&&(const MyClass& other) const {
    // Implementation of the logical AND operator
}

bool operator||(const MyClass& other) const {
    // Implementation of the logical OR operator
}

bool operator!() const {
    // Implementation of the logical NOT operator
}

Arithmetic Operators

The arithmetic operators (+, -, *, /, %) are used for arithmetic operations. They can be overloaded to provide custom behavior for arithmetic operations involving objects of your class type. Here's an example:

MyClass operator+(const MyClass& other) const {
    // Implementation of the addition operator
}

MyClass operator-(const MyClass& other) const {
    // Implementation of the subtraction operator
}

MyClass operator*(const MyClass& other) const {
    // Implementation of the multiplication operator
}

MyClass operator/(const MyClass& other) const {
    // Implementation of the division operator
}

MyClass operator%(const MyClass& other) const {
    // Implementation of the modulus operator
}

Subscript Operator

The subscript operator ([]) is used for array-like access to objects. It can be overloaded to enable array-like behavior for objects of your class type. Here's an example:

return_type operator[](index_type index) const {
    // Implementation of the subscript operator
}

Operators for Pointer-like Types

For classes that behave like pointers, you can overload the dereference (*) and arrow (->) operators to provide custom behavior. Here's an example:

return_type operator*() const {
    // Implementation of the dereference operator
}

return_type operator->() const {
    // Implementation of the arrow operator
}

Comparison Operators, Including C++20 Three-Way Comparison

The comparison operators (==, !=, <, <=, >, >=) are used for comparing objects. They can be overloaded to provide custom behavior for comparisons involving objects of your class type. Here's an example:

bool operator==(const MyClass& other) const {
    // Implementation of the equality operator
}

bool operator!=(const MyClass& other) const {
    // Implementation of the inequality operator
}

bool operator<(const MyClass& other) const {
    // Implementation of the less-than operator
}

bool operator<=(const MyClass& other) const {
    // Implementation of the less-than-or-equal-to operator
}

bool operator>(const MyClass& other) const {
    // Implementation of the greater-than operator
}

bool operator>=(const MyClass& other) const {
    // Implementation of the greater-than-or-equal-to operator
}

Conversion Operators

Conversion operators allow implicit conversion between types. They can be overloaded to provide custom conversion behavior for objects of your class type. Here's an example:

operator return_type() const {
    // Implementation of the conversion operator
}

Overloading new and delete

The new and delete operators are used for dynamic memory allocation and deallocation. They can be overloaded to provide custom memory management behavior for objects of your class type. Here's an example:

void* operator new(std::size_t size) {
    // Implementation of the new operator
}

void operator delete(void* ptr) {
    // Implementation of the delete operator
}

Summary of Canonical Function Signatures

Here is a summary of the canonical function signatures for operator overloading in C++:

MyClass operatorOPERATOR(parameters) {
    return value;
}

bool operatorOPERATOR(const MyClass& other) const {
    return value;
}

return_type operator()(parameters) {
    return value;
}

bool operator&&(const MyClass& other) const {
    return value;
}

bool operator||(const MyClass& other) const {
    return value;
}

bool operator!() const {
    return value;
}

MyClass operator+(const MyClass& other) const {
    return value;
}

MyClass operator-(const MyClass& other) const {
    return value;
}

MyClass operator*(const MyClass& other) const {
    return value;
}

MyClass operator/(const MyClass& other) const {
    return value;
}

MyClass operator%(const MyClass& other) const {
    return value;
}

return_type operator[](index_type index) const {
    return value;
}

return_type operator*() const {
    return value;
}

return_type operator->() const {
    return value;
}

bool operator==(const MyClass& other) const {
    return value;
}

bool operator!=(const MyClass& other) const {
    return value;
}

bool operator<(const MyClass& other) const {
    return value;
}

bool operator<=(const MyClass& other) const {
    return value;
}

bool operator>(const MyClass& other) const {
    return value;
}

bool operator>=(const MyClass& other) const {
    return value;
}

operator return_type() const {
    return value;
}

void* operator new(std::size_t size) {
    return pointer;
}

void operator delete(void* ptr) {
    // Implementation of the delete operator
}