Defining Custom Behaviors for C++ Operators
The C++ language allows operators to be redefined sothat they work with user‑defined types in a natural way. This mechanism is called operator overloading and is essentially syntactic sugar for function calls.
Adding Two Custom Objects
When you need to combine two objects of a class, you can provide a dedicated member function, but overloading + makes the code much cleaner.
Member Function Approach
#include <iostream>
using namespace std;
class Vector2D {
public:
int x;
int y;
Vector2D(int xVal, int yVal) : x(xVal), y(yVal) {}
Vector2D operator+(const Vector2D &other) const {
return Vector2D(x + other.x, y + other.y);
}
};
int main() {
Vector2D v1(3, 7);
Vector2D v2(8, 2);
Vector2D v3 = v1 + v2;
cout << "Result: (" << v3.x << ", " << v3.y << ")" << endl;
return 0;
}
Global Function Approach
#include <iostream>
using namespace std;
class Vector2D {
public:
int x;
int y;
Vector2D(int xVal, int yVal) : x(xVal), y(yVal) {}
};
Vector2D operator+(const Vector2D &lhs, const Vector2D &rhs) {
return Vector2D(lhs.x + rhs.x, lhs.y + rhs.y);
}
int main() {
Vector2D v1(5, 9);
Vector2D v2(2, 3);
Vector2D v3 = v1 + v2;
cout << "Sum: (" << v3.x << ", " << v3.y << ")" << endl;
return 0;
}
Overloaded operators can themselves be overloaded — different parameter lists allow the same operator symbol to work with multiple type combinations.
#include <iostream>
using namespace std;
class Vector2D {
public:
int x, y;
Vector2D(int xVal, int yVal) : x(xVal), y(yVal) {}
};
Vector2D operator+(const Vector2D &v, int scalar) {
return Vector2D(v.x + scalar, v.y + scalar);
}
int operator+(const Vector2D &v, const int &magic) {
return v.x + v.y + magic;
}
int main() {
Vector2D a(1, 4);
Vector2D b(6, 2);
Vector2D c = a + b; // uses Vector2D + Vector2D
Vector2D d = a + 10; // uses Vector2D + int -> Vector2D
int total = a + 100; // uses Vector2D + const int -> int
cout << "d: (" << d.x << ", " << d.y << ")" << endl;
cout << "Total: " << total << endl;
return 0;
}
Key points:
- Only user‑defined types can participate in operator overloading; built‑in operand meanings cannot be altered.
- Keep the semantics intuitive — avoid surprises that reduce readability.
Streaming Output with <<
To print objects directly with cout, overload the left‑shift operator. Since cout is an ostream object, a global overload with a friend declaration works best.
Incorrect Member Overload
Defining << as a member forces the object to appear on the left, producing awkward syntax.
#include <iostream>
using namespace std;
class Point {
int px, py;
public:
Point(int xx, int yy) : px(xx), py(yy) {}
ostream& operator<<(ostream &os) {
os << "(" << px << ", " << py << ")";
return os;
}
};
int main() {
Point p1(12, 5);
Point p2(7, 30);
(p2 << (p1 << cout)) << endl;
return 0;
}
Correct Global Overload with Friend
#include <iostream>
using namespace std;
class Point {
friend ostream& operator<<(ostream &os, const Point &pt);
private:
int px, py;
public:
Point(int xx, int yy) : px(xx), py(yy) {}
};
ostream& operator<<(ostream &os, const Point &pt) {
os << "[" << pt.px << ", " << pt.py << "]";
return os;
}
int main() {
Point a(4, 9);
Point b(10, 2);
cout << a << " | " << b << endl;
return 0;
}
Returning a reference to ostream enables chaining. If data members are private, either add public getters or declare the global operator as a friend.
Increment and Decrement Operators
Overloading ++ and -- requires handling both prefix and postfix forms. The postfix version accepts a dummy int parameter to differantiate it from the prefix form.
#include <iostream>
using namespace std;
class Counter {
friend ostream& operator<<(ostream &os, const Counter &c);
private:
int value;
public:
Counter() : value(0) {}
// Prefix ++: increments and returns a reference for chaining
Counter& operator++() {
++value;
return *this;
}
// Postfix ++: saves state, increments, returns constant copy
const Counter operator++(int) {
Counter previous = *this;
value++;
return previous;
}
};
ostream& operator<<(ostream &os, const Counter &c) {
os << c.value;
return os;
}
int main() {
Counter c1, c2;
cout << ++c1 << endl; // 1
cout << ++++c1 << endl; // 3 (chained prefix)
cout << c2++ << endl; // 0
cout << c2 << endl; // 1
// ++(c2++); // compilation error (postfix returns const)
return 0;
}
For global overloads, the object must be passed by reference and the operators should often be friends.
#include <iostream>
using namespace std;
class Meter {
friend ostream& operator<<(ostream &os, const Meter &m);
friend Meter& operator--(Meter &m);
friend const Meter operator--(Meter &m, int);
private:
int length;
public:
Meter(int l) : length(l) {}
};
ostream& operator<<(ostream &os, const Meter &m) {
os << m.length << "m";
return os;
}
Meter& operator--(Meter &m) {
--m.length;
return m;
}
const Meter operator--(Meter &m, int) {
Meter old = m;
m.length--;
return old;
}
int main() {
Meter m1(5), m2(10);
cout << --m1 << endl;
cout << m2-- << endl;
cout << m2 << endl;
return 0;
}
Prefix operators should return a non‑const reference to allow chain expressions. Postfix versions should return a const object to prevent misuse such as obj++++.
Assignment Operator and Deep Copy
The compiler generates a default operator= that performs member‑wise shallow copy. When a class manages heap resources, a custom version is essential.
#include <iostream>
using namespace std;
class Buffer {
int *data;
public:
Buffer(int val) {
data = new int(val);
}
~Buffer() {
delete data;
data = nullptr;
}
// Deep‑copy assignment
Buffer& operator=(const Buffer &src) {
if (this == &src) return *this; // self‑assignment guard
delete data;
data = new int(*src.data);
return *this;
}
int value() const { return *data; }
};
int main() {
Buffer b1(42);
Buffer b2(7);
b2 = b1;
cout << b2.value() << endl; // 42
return 0;
}
Choosing between returning a reference or a value matters:
- Returning
Buffer&avoids extra copy construction calls. - Returning
Bufferby value triggers a copy constructor, which must itself perform a deep copy to prevent double deletion.
Relational Operators
Relational operators (==, !=, <, etc.) let you compare objects naturally.
#include <iostream>
#include <string>
using namespace std;
class Student {
string id;
int grade;
public:
Student(string id, int grade) : id(id), grade(grade) {}
bool operator==(const Student &other) const {
return id == other.id && grade == other.grade;
}
friend bool operator!=(const Student &a, const Student &b) {
return !(a == b);
}
};
int main() {
Student alice("A01", 85);
Student bob("A02", 85);
cout << boolalpha << (alice == bob) << endl; // false
cout << (alice != bob) << endl; // true
return 0;
}
Function Call Operator (Functors)
Overloading operator() turns objects into callable entities known as functors.
#include <iostream>
#include <string>
using namespace std;
class Logger {
public:
void operator()(const string &msg) {
cout << "[LOG] " << msg << endl;
}
int operator()(int a, int b) {
return a * b;
}
};
int main() {
Logger log;
log("System started");
int product = log(6, 7);
cout << product << endl; // 42
// Using an anonymous object
cout << Logger()(2, 3) << endl;
return 0;
}
Note that operator() must be defined as a non‑static member function; it cannot appear at global scope.