C++’s overload resolution mechanism chooses a function or operator definition from various choices when user-defined types have overloaded functions or operators. For polymorphism to be enabled at compile time, this procedure is essential.
Overloading Operators in C++ for User-Defined Types
One of C++’s most useful features is operator overloading, which lets you specify what an existing operator implies when it’s used to objects of your own class types. In essence, it is a type of overloading, which is the use of the same name (or symbol, for operators) but alternative implementations according to the types of parameters or the amount of parameters.
Purpose and Benefits
- Intuitive and Readable Code: Your programs will be easier to read and write if operators are overloaded. As with built-in types, for example, you can write d3 = d1 + d2; for custom types instead of d3.addobjects(d1, d2);.
- Integration with Standard Library: By defining operators such as < for your custom types, you can use them with data structures like sets and associative arrays and Standard Library algorithms like sort(), giving them a lot of capability with little more description.
- Extending C++: You can utilise objects in expressions exactly like you can with C++’s built-in data types with its ability to fully integrate new class types into the programming environment.
Rules and Restrictions for Overloading Operators
C++ restricts operator overloading in a number of ways to guard against abuse and preserve consistency:
- Existing Operators Only: Existing C++ operators can only be overloaded; new operator symbols cannot be created. The exponentiation operator **, for instance, cannot be specified.
- Cannot Redefine for Built-in Types: Operands of built-in types cannot have their meaning altered by an operator. An error would be int operator+(int, int), for instance.
Preserve Core Characteristics
The basic properties of an operator remain unchanged when it is overloaded:
- Number of Operands: While a binary operator stays binary, a unary operator stays unary.
- Precedence: An operator that is overloaded has the same precedence as its built-in counterpart.
- Associativity: There is also no change in the associativity (left-to-right or right-to-left grouping).
Operators That Cannot Be Overloaded
The following operators are specifically not overloadable:
- Scope resolution operator (::)
- Member access operator (.)
- Pointer-to-member operator (.*)
- Conditional operator (?:)
- Typeid, sizeof, and alignof are named operators that report basic facts.
- The preprocessor # and ## operators
Consistency is Key
The definition of overloaded operators to carry out activities akin to their inherent meanings is often a good idea. Operator+ should still suggest addition or concatenation, for instance.
Avoid Overloading Short-Circuit Operators
Generally speaking, it is not recommended to overload the logical AND (&&), logical OR (||), comma (,), and address-of (&) operators because doing so destroys short-circuit evaluation or may create unexpected behaviour in contrast to their inherent semantics.
Defining Overloaded Operator Functions
Overloaded operators are indicated by the operator symbol after the keyword operator since they are special functions. Like any function, an overloaded operator has a body, parameters, and return type.
Syntax:
return-type class-name :: operator op(arg-list) { // function body }
or for non-member functions:
return-type operator op(arg-list) { // function body }
Member vs. Non-Member Functions
Member Functions:
- An implicit binding of the left-hand operand to this pointer occurs when an operator is defined as a member function.
- If the operator is unary, the member function does not require any explicit parameters. This object serves as the operand.
- For a binary operator, the right-hand operand is represented by the single explicit parameter that the member function accepts.
- Non-static member functions must be specified for the assignment (=), subscript ([]), function call (()), and arrow (->) operators.
Non-Member (Friend) Functions:
- To access the private members of a class, non-member overloaded operator functions are usually specified as buddy functions.
- They explicitly receive all of their operands.
- Friendship functions for unary operators accept a single parameter.
- Two parameters are required for a friend function of binary operators.
- It is often desirable to implement symmetric operators as ordinary non-member functions in order to allow implicit type conversions for both operands. These operators include arithmetic (+, -, *, /), equality (==,!=), relational (<, >, <=, >=), and bitwise operators.
Prefix vs. Postfix Increment/Decrement
- An additional (unused) int parameter is needed to distinguish increment (++) and decrement (–) operators in their prefix (++obj) and postfix (obj++) forms. For this int parameter, the compiler passes 0 to the postfix operator.
- To save code duplication, the postfix operator’s functionality is frequently implemented by invoking its prefix equivalent.
Calling Overloaded Operator Functions Directly
With their unique operator name, overloaded operator functions can be called directly like any other function, even though they are usually employed indirectly (a + b, for example). For a non-member function, for example, data1 + data2 is equal to operator+(data1, data2). The equation would be data1.operator+(data2) if it were a member function.
Code Example: Overloading Operators for a Struct
Using a USCurrency struct, let’s define the addition (+), compound addition (+=), and output (<<) operators to demonstrate operator overloading.
#include <iostream>
#include <iomanip>
struct USCurrency {
int dollars;
int cents;
USCurrency(int d = 0, int c = 0) : dollars(d), cents(c) {
dollars += cents / 100;
cents %= 100;
if (cents < 0) {
dollars--;
cents += 100;
}
}
USCurrency operator+(const USCurrency& other) const {
USCurrency result;
result.dollars = dollars + other.dollars;
result.cents = cents + other.cents;
result.dollars += result.cents / 100;
result.cents %= 100;
return result;
}
USCurrency& operator+=(const USCurrency& other) {
dollars += other.dollars;
cents += other.cents;
dollars += cents / 100;
cents %= 100;
if (cents < 0) {
dollars--;
cents += 100;
}
return *this;
}
};
std::ostream& operator<<(std::ostream& os, const USCurrency& currency) {
os << "$" << currency.dollars << "." << std::setw(2) << std::setfill('0') << currency.cents;
return os;
}
std::istream& operator>>(std::istream& is, USCurrency& currency) {
char dollar_sign, dot;
is >> dollar_sign >> currency.dollars >> dot >> currency.cents;
if (is.fail() || dollar_sign != '$' || dot != '.') {
is.setstate(std::ios_base::failbit);
currency.dollars = 0;
currency.cents = 0;
} else {
currency.dollars += currency.cents / 100;
currency.cents %= 100;
if (currency.cents < 0) {
currency.dollars--;
currency.cents += 100;
}
}
return is;
}
int main() {
USCurrency amount1(10, 50);
USCurrency amount2(5, 75);
USCurrency amount3(0, 125);
USCurrency amount4(20, 0);
USCurrency sum;
sum = amount1 + amount2;
std::cout << "Amount1: " << amount1 << "\n";
std::cout << "Amount2: " << amount2 << "\n";
std::cout << "Sum (amount1 + amount2): " << sum << "\n\n";
std::cout << "Amount4 before +=: " << amount4 << "\n";
amount4 += amount3;
std::cout << "Amount4 after += Amount3: " << amount4 << "\n\n";
USCurrency user_amount;
std::cout << "Enter a currency amount (e.g., $15.99): ";
std::cin >> user_amount;
if (std::cin.good()) {
std::cout << "You entered: " << user_amount << "\n";
} else {
std::cout << "Invalid input format.\n";
}
return 0;
}
Output
Amount1: $10.50
Amount2: $5.75
Sum (amount1 + amount2): $16.25
Amount4 before +=: $20.00
Amount4 after += Amount3: $21.25
Enter a currency amount (e.g., $15.99): $15.99
You entered: $15.99
You can also read Const Member Functions In C++: What They Are & Why It Matter