Operator Overloading is an inbuilt feature of the CPP programming language that enables
you to write your own definition of an operator for user-defined data types. It allows you to
define how operators such as +, -, *, /, etc., behave when applied to objects of your classes.
This can make your code more intuitive and easier to read, as you can use standard operators.
For example:
#include <iostream>
class Num
{
public:
int num;
Num(int n) : num(n) {}
Num operator +(Num &n)
{
return Num(num - n.num);
}
Num operator -(Num &n)
{
return Num(num + n.num);
}
};
int main()
{
Num n1(10), n2(5);
Num n3 = n1 + n2; // Calls the operator+ function
Num n4 = n1 - n2; // Calls the operator- function
std::cout << "n3: " << n3.num << std::endl; // Output: n3: 5
std::cout << "n4: " << n4.num << std::endl; // Output: n4: 15
// The output is 5 and 15, which is the opposite of what we would expect from the operator overloads.
return 0;
}
As you can observe that strangely, the operators have flipped in functionality for the class Num
due to operator overloading
The following operators can be overloaded in C++:
The following code demonstrates how to overload the basic arithmetic, relational, and logical operators in C++ within a class:
#include <iostream>
/*
To overload an operator in C++, you define a function with the following syntax:
Return-Type operator operatorName (parameter-list) { function-body }
*/
class Num
{
public:
int num;
Num(int n) : num(n) {}
// Overloading the generic math operators
Num operator+(const Num &other) const
{
return Num(num + other.num);
}
// using macros to reduce code duplication (pages coming soon...)
#define ARITHMETIC_OPERATOR(op) \
Num operator op (const Num &other) const \
{ \
return Num(num op other.num); \
}
ARITHMETIC_OPERATOR(-)
ARITHMETIC_OPERATOR(*)
ARITHMETIC_OPERATOR(/)
ARITHMETIC_OPERATOR(%)
};
/*
Overloading can also be done from outside the class, but it is less common.
For example, you can overload the operator+ function like this:
*/
Num operator+(const Num &n1, const Num &n2) {
return Num(n1.num + n2.num);
}
int main()
{
Num n1(10), n2(5);
Num n3 = n1 + n2; // Calls the operator+ function
Num n4 = n1 - n2; // Calls the operator- function
Num n5 = n1 * n2; // Calls the operator* function
Num n6 = n1 / n2; // Calls the operator/ function
Num n7 = n1 % n2; // Calls the operator% function
std::cout << "n1 + n2 = " << n3.num << std::endl;
std::cout << "n1 - n2 = " << n4.num << std::endl;
std::cout << "n1 * n2 = " << n5.num << std::endl;
std::cout << "n1 / n2 = " << n6.num << std::endl;
std::cout << "n1 % n2 = " << n7.num << std::endl;
// The overloaded operator+ function can also be called like this:
Num n8 = operator+(n1, n2); // Calls the operator+ function
std::cout << "operator+(n1, n2) = " << n8.num << std::endl;
return 0;
}
Similarly relational operators can also be overloaded in the same way as arithmetic operators. For example:
#include <iostream>
class Num
{
public:
int num;
Num(int n) : num(n) {}
};
/*
Overloading the operator > for the Num class.
Here the > operator instead of returning traditional true or false, it returns the max among the objects.
*/
Num operator>(const Num &n1, const Num &n2) {
return n1.num > n2.num ? n1 : n2;
}
int main()
{
Num n1(10), n2(5);
std::cout << "Max: " << (n1 > n2).num << std::endl; // Output: Max: 10
// The overloaded operator > returns the object with the greater num value.
// In this case, it returns n1 since 10 > 5.
return 0;
}
Similarly, you can overload the logical operators as well. For example:
#include <iostream>
#include <string>
class Num
{
public:
int num;
Num(int n) : num(n) {}
};
/*
Overloading the operator && for the Num class.
Here the && operator instead of returning traditional true or false, it returns a string
*/
std::string operator&&(const Num &n1, const Num &n2) {
return n1.num == n2.num ? "Equal" : "Not Equal";
}
int main()
{
Num n1(10), n2(5);
std::cout << "Equality: " << (n1 && n2) << std::endl; // Output: Equality: Not Equal
// The overloaded operator && returns a string instead of a boolean value.
return 0;
}
Similar techinques can be applied for bitwise operators to modify their traditional behaviour
As you might have observed that the operator overloading construct doesnt explicitly specify the order of the operators applied.
Which means that you actually can't specify ++ operator(){...}
as it is illegal.
But then how to overload the pre increment and post increment operators?
class Num
{
public:
int num;
Num(int n) : num(n) {} // Constructor to initialize num
// Overloading pre-increment (++Num)
Num &operator++()
{
++num; // Increment the value
return *this; // Return the current object
}
// Overloading post-increment (Num++)
Num operator++(int)
{ // <<< NOTE THE (int) INSIDE THE PARENTHESES!!!
Num temp = *this; // Save the current state
++num; // Increment the value
return temp; // Return the old state
}
};
int main() {
Num n1(10);
std::cout << "Initial value: " << n1.num << std::endl;
++n1; // Pre-increment
std::cout << "After pre-increment: " << n1.num << std::endl;
Num n2 = n1++; // Post-increment
std::cout << "After post-increment: " << n1.num << std::endl;
std::cout << "Value of n2 (before increment): " << n2.num << std::endl;
return 0;
}
Did you see it? We simply use the int inside the parentheses of the post increment operator overload function to differentiate it from the pre increment operator overload function.
The assignment operator is a special operator in C++ that allows you to assign values from one object to another.
By default, the assignment operator performs a shallow copy of the object's members. However, you can overload the assignment operator to perform a deep copy or customize the assignment behavior.
Here's an example of how to overload the assignment operator:
#include <iostream>
#include <string>
#include <cstdlib> // For std::stoi
class Num
{
public:
int num;
Num(int n) : num(n) {} // Constructor to initialize num
// Assignment operator overloading
Num& operator=(std::string numStr)
{
num = std::stoi(numStr); // Convert string to int and assign
return *this;
}
};
int main()
{
Num n1(10);
std::cout << "Initial Value: " << n1.num << std::endl; // Output: 10
n1 = "20"; // Assigning a string to n1 using overloaded operator= MAGIC!!!
std::cout << "After Assignment: " << n1.num << std::endl; // Output: 20
return 0;
}
The subscript operator []
can be overloaded to provide custom behavior for array-like access in your classes.
Here's an example of how to overload the subscript operator:
#include <iostream>
class Num
{
public:
int num;
Num(int n) : num(n) {} // Constructor to initialize num
// Subscript operator overloading
int operator[](int index) {
return num >> index && 1; // Right shift and bitwise AND to get the bit at index
}
};
int main()
{
Num n1(10);
std::cout << "Bit at index 3: " << n1[3] << std::endl; // Should print 1 (10 in binary is 1010)
// ^xxx
return 0;
}
The method call operator ()
can be overloaded to allow objects of your class to be called like functions.
Here's an example of how to overload the method call operator:
#include <iostream>
#include <string>
class Num
{
public:
int num;
Num(int n) : num(n) {} // Constructor to initialize num
// Subscript operator overloading
std::string operator()(int repeat) {
return std::string(repeat, 65 + num); // Right shift and bitwise AND to get the bit at index
}
};
int main()
{
Num n1(17);
std::cout << "Bit at index 3: " << n1(3) << std::endl; // Should print 'R' 3 times as RRR
return 0;
}
The member access operator ->
can also be overloaded for special cases to provide custom behavior when accessing members of a class.
Here's an example of how to overload the member access operator:
#include <iostream>
class Num
{
public:
int num;
Num(int n) : num(n) {} // Constructor to initialize num
};
class NumProxy
{
public:
Num #
NumProxy(Num& n, int index) : num(n) {}
Num* operator ->()
{
return # // Return the address of num
}
};
int main()
{
Num n1(17);
NumProxy n1p(n1, 0); // Create a NumProxy object
std::cout << "Number by proxy: " << n1p->num << std::endl; // Should print 17
return 0;
}
The stream insertion operator <<
and the stream extraction operator >>
can be overloaded to allow your class objects to be used with input/output streams.
Here's an example of how to overload these operators:
#include <iostream>
class Num
{
public:
int num;
Num(int n) : num(n) {} // Constructor to initialize num
// this function is actually overloading the << operator of the ostream class thus requiring the friend keyword
// to allow access to private members of the class
friend std::ostream& operator <<(std::ostream& os, const Num& n)
{
os << "Num: " << n.num;
return os;
}
};
/* you can also overload teh << operator outside the class
std::ostream& operator <<(std::ostream& os, const Num& n)
{
os << "Num: " << n.num;
return os;
}
*/
int main()
{
Num n1(17);
std::cout << n1 << '\n'; // Output: Num: 17
return 0;
}
Literal operators are a special type of operator in C++ that allows you to define custom behavior for user-defined literals.
This feature is particularly useful when you want to create a more readable syntax for your classes.
Here's an example of how to define a user-defined literal operator:
#include <iostream>
class Num
{
public:
int num;
Num(int n) : num(n) {} // Constructor to initialize num
};
Num operator ""N(unsigned long long n)
{
return Num(n);
}
int main()
{
Num n1 = 17N; // Using the user-defined literal operator
std::cout << "Number from literal: " << n1.num << '\n'; // Output: Num: 17
return 0;
}
Using operator overloading to implement a simple complex number class.
#include <iostream>
// a simple complex number class to demonstrate operator overloading
// the class has been made to resemble C++ complex library as close as possible to demonstrate the power of operator overloading
class Complex
{
public:
float real, complex;
Complex(float x, float y) : real(x), complex(y) {}
// used to define basic operators
#define OPERATOR(op) \
Complex operator op(const Complex& other) const \
{ \
return Complex(real + other.real, complex + other.complex); \
}
OPERATOR(+)
OPERATOR(-)
#undef OPERATOR
// define multiplication among complex numbers
Complex operator *(const Complex& other) const
{
return Complex(real * other.real - complex * other.complex, real * other.complex + complex * other.real);
}
// define division among complex numbers
Complex operator /(const Complex& other) const
{
float denom = other.real * other.real + other.complex * other.complex;
return Complex((real * other.real + complex * other.complex) / denom, (complex * other.real - real * other.complex) / denom);
}
};
// make the complex number literal operator, this creates the imaginary part of the complex number
Complex operator ""i(long double y)
{
return Complex(0, static_cast<float>(y));
}
// support addition of a real number with complex number as real + complex
Complex operator +(double x, const Complex &c )
{
return Complex(static_cast<float>(x) + c.real, c.complex);
}
// support subtraction of a real number with complex number as real + complex
Complex operator -(double x, const Complex &c )
{
return Complex(static_cast<float>(x) - c.real, -c.complex);
}
// overload the << operator to print complex numbers in the form a + bi
std::ostream& operator<<(std::ostream& os, const Complex& c)
{
os << c.real << (c.complex >= 0 ? "+" : "") << c.complex << "i";
return os;
}
// driver code
int main()
{
Complex cmp1 = 1.0 + 2.0i;
Complex cmp2 = 3.0 - 4.0i;
std::cout << "Complex sum: " << cmp1 + cmp2 << '\n'; // output: 4 - 2i
std::cout << "Complex divide: " << cmp1 / cmp2 << '\n'; // output: -0.2 + 0.4i
return 0;
}
As you can see that the complex number class resembles the C++ complex library as close as possible.
This is the power of operator overloading, it allows you to create custom data types that behave like built-in types, making your code more intuitive and easier to read.