Lesson 32
Operator Overloading
Covers operator overloading syntax, member vs non-member operator functions, overloadable and non-overloadable operators, and the rules of operator overloading.
Lesson content
What is operator overloading?
Operator overloading is the process of giving an existing C++ operator a new meaning when it is used with objects of a user-defined class. The operator itself is not changed, its behavior is extended for your custom types.
- 5 + 10 adds two integers
- "Hello" + "World" (in some languages) concatenates strings
- c1 + c2 adds two complex numbers
The symbol remains the same, but its behavior depends on the operands.
Why does operator overloading exist?
Without operator overloading, code can become verbose and less intuitive. Instead of writing Point p3 = addPoints(p1, p2);, you can write Point p3 = p1 + p2;.
- Cleaner code
- Improved readability
- Natural syntax
- Easier maintenance
- Better abstraction
Real-world use cases
- Mathematical libraries
- Complex number calculations
- Matrix operations
- Geometry applications
- Game engines
- Graphics programming
- Standard library classes (such as streams)
Prerequisites
- Classes and objects
- Constructors
- Function overloading
- Member functions
- Basic Object-Oriented Programming concepts
Basic syntax
Operator overloading is implemented using a special function named operator.
returnType operatorOperatorSymbol(parameterList)
{
// Function body
}
Complex operator+(const Complex& other)
{
// Implementation
}General syntax
class ClassName
{
public:
returnType operator+(parameters)
{
// Code
}
};Operators that can be overloaded
- Arithmetic: +, -, *, /, %, ++
- Comparison: ==, !=, <, >, <=, >=
- Assignment: =, +=, -=, *=, /=, %=
- Other: [], (), ->, <<, >>, new, delete
Operators that cannot be overloaded
- . – member access
- :: – scope resolution
- ?: – conditional operator
- sizeof – size operator
- typeid – type information
- .* – pointer-to-member access
Member vs non-member operator functions
As a member function, used when the left-hand operand is an object of the class.
Complex operator+(const Complex& other);As a non-member function, often implemented as a friend when access to private members is required.
Complex operator+(const Complex& left, const Complex& right);Rules of operator overloading
- At least one operand must be a user-defined type
- You cannot create new operators
- You cannot change an operator's precedence
- You cannot change an operator's associativity
- You cannot change the number of operands an operator takes
- Overloading should preserve the operator's expected meaning whenever possible
Example 1: Overloading the + operator
#include <iostream>
using namespace std;
class Complex
{
public:
int real;
int imaginary;
Complex(int r, int i)
{
real = r;
imaginary = i;
}
Complex operator+(const Complex& other)
{
return Complex(
real + other.real,
imaginary + other.imaginary
);
}
void display()
{
cout << real << " + " << imaginary << "i" << endl;
}
};
int main()
{
Complex c1(2, 3);
Complex c2(4, 5);
Complex c3 = c1 + c2;
c3.display();
return 0;
}
// Output: 6 + 8iExample 2: Overloading the == operator
#include <iostream>
using namespace std;
class Student
{
public:
int id;
Student(int studentId)
{
id = studentId;
}
bool operator==(const Student& other)
{
return id == other.id;
}
};
int main()
{
Student s1(101);
Student s2(101);
if (s1 == s2)
{
cout << "Equal" << endl;
}
return 0;
}
// Output: EqualExample 3: Overloading the prefix ++ operator
#include <iostream>
using namespace std;
class Counter
{
private:
int value;
public:
Counter()
{
value = 0;
}
Counter operator++()
{
++value;
return *this;
}
void display()
{
cout << value << endl;
}
};
int main()
{
Counter counter;
++counter;
counter.display();
return 0;
}
// Output: 1Example 4: Overloading the stream insertion (<<) operator
#include <iostream>
using namespace std;
class Point
{
private:
int x;
int y;
public:
Point(int xValue, int yValue)
{
x = xValue;
y = yValue;
}
friend ostream& operator<<(ostream& output, const Point& point)
{
output << "(" << point.x << ", " << point.y << ")";
return output;
}
};
int main()
{
Point point(10, 20);
cout << point << endl;
return 0;
}
// Output: (10, 20)Example 5: Overloading the assignment (=) operator
#include <iostream>
using namespace std;
class Number
{
private:
int* value;
public:
Number(int number)
{
value = new int(number);
}
Number& operator=(const Number& other)
{
if (this != &other)
{
*value = *other.value;
}
return *this;
}
~Number()
{
delete value;
}
};
int main()
{
Number first(10);
Number second(20);
first = second;
return 0;
}Common mistakes and pitfalls
- Overloading operators with unrelated meanings. Fix: preserve the operator's natural behavior
- Forgetting to pass large objects by const reference. Fix: use const ClassName& to avoid unnecessary copies
- Returning by value when a reference is expected (e.g., assignment operator). Fix: return ClassName& for assignment operators
- Forgetting to handle self-assignment in operator=. Fix: check if (this != &other) before copying
- Making overloaded operators modify operands unexpectedly. Fix: keep behavior predictable and consistent
Best practices
- Overload operators only when the meaning is intuitive
- Pass operands by const reference whenever possible
- Return references where appropriate (such as operator=)
- Mark operator functions as const if they don't modify the object
- Implement related operators consistently (for example, if you overload ==, consider overloading != as well)
- Prefer non-member or friend functions when the left operand isn't naturally the current object (such as << and >>)
When not to use this
- It makes code harder to understand
- The operator's new behavior is surprising or unrelated to its usual meaning
- A clearly named member function communicates intent better
- Your class doesn't represent a concept where operators are naturally applicable
Summary
- Operator overloading lets user-defined types work with familiar C++ operators
- It improves readability and provides natural syntax for custom classes
- Many operators can be overloaded, but some (such as ., ::, and ?:) cannot
- At least one operand must be a user-defined type
- Operator overloading is a form of compile-time polymorphism
- Use const references and appropriate return types for efficiency
- Keep overloaded operators intuitive and consistent with their expected meanings
Frequently asked questions
What is operator overloading in C++?
Operator overloading allows built-in operators to work with user-defined classes by defining custom implementations for those operators.
Can every operator be overloaded?
No. Operators such as ., ::, ?:, sizeof, and typeid cannot be overloaded.
What is the difference between function overloading and operator overloading?
Function overloading uses multiple functions with the same name and different parameter lists, while operator overloading gives existing operator symbols like + and == custom behavior for user-defined types. Both are forms of compile-time polymorphism.
Why is the << operator often implemented as a friend function?
The left operand of << is usually an ostream object (such as cout), not an object of your class. A non-member friend function allows access to private members while supporting expressions like cout << object;.
When should I overload an operator?
Overload an operator only when it makes your class easier to use and its behavior matches what programmers naturally expect. For example, overloading + for mathematical objects like vectors or complex numbers is appropriate, while overloading it for unrelated actions is generally discouraged.