Operator overloading
Inheritance, operators, and templates13 min readLesson 4 of 5
4.1 Operators are functions
An operator overload is a function whose name is operator followed by the symbol. a + b calls operator+(a, b) when either operand has a class type. The compiler still applies the built-in precedence and arity, so overloading only chooses which function runs.
An operator can be a member function or a free function. A member operator+ receives the right operand as its single parameter and the left operand as this. A free function receives both operands. At least one operand must be a class or enumeration type, and operators such as ::, ., ?:, and sizeof cannot be overloaded.
#include <iostream>
struct Vec2 {
double x = 0.0;
double y = 0.0;
};
Vec2 operator+(const Vec2& a, const Vec2& b) {
return Vec2{a.x + b.x, a.y + b.y};
}
bool operator==(const Vec2& a, const Vec2& b) {
return a.x == b.x && a.y == b.y;
}
int main() {
Vec2 a{1.0, 2.0};
Vec2 b{3.0, 4.0};
Vec2 c = a + b;
std::cout << "(" << c.x << ", " << c.y << ")\n";
std::cout << std::boolalpha << "Equal: " << (a == b) << "\n";
return 0;
}Output
(4, 6) Equal: false
4.1.1 Writing to a stream
std::cout << value calls operator<<(std::cout, value). The left operand is a stream, so this operator is a free function. It takes std::ostream& and the value, writes the value, and returns the stream so insertions can chain.
A stream operator should not print a newline unless the type's documented format includes one. Callers decide how lines are arranged. If the operator needs private data, declare it a friend or provide const getters. A const reference parameter keeps the object from being copied and prevents the operator from changing it.
#include <iostream>
struct Vec2 {
double x = 0.0;
double y = 0.0;
};
std::ostream& operator<<(std::ostream& out, const Vec2& v) {
out << "(" << v.x << ", " << v.y << ")";
return out;
}
int main() {
Vec2 a{1.0, 2.0};
std::cout << "Point: " << a << "\n";
return 0;
}Output
Point: (1, 2)
4.1.2 Compound assignment and arithmetic
operator+= changes the left object and returns a reference to it. Returning *this by reference matches the behavior of the built-in += and allows chained updates. operator+ can then take its left operand by value, add the right operand, and return the result.
This pairing keeps the arithmetic logic in one place. It also makes clear which operation mutates and which one produces a new value. If a type supports ==, decide whether != should also exist and implement it in terms of == so the two never disagree.
#include <iostream>
class Counter {
public:
explicit Counter(int value = 0) : value_(value) {}
Counter& operator+=(int amount) { value_ += amount; return *this; }
int value() const { return value_; }
private:
int value_;
};
Counter operator+(Counter left, int amount) { left += amount; return left; }
int main() {
Counter count(5);
count += 3;
Counter total = count + 2;
std::cout << "Count: " << count.value() << "\nTotal: " << total.value() << "\n";
return 0;
}Output
Count: 8 Total: 10
4.1.3 Ordering objects
std::sort and ordered containers use operator< by default. The comparison must define a strict weak ordering: an object never compares less than itself, and the relation is transitive. A comparison that treats two different objects as neither less than the other is allowed, and it makes them equivalent for sorting.
When several fields matter, compare the most important field first and fall through to the next field for ties. That gives a stable, predictable order. Define the operator as a free function taking two const references so it works with either operand on the left.
#include <algorithm>
#include <iostream>
#include <string>
#include <vector>
struct Student {
std::string name;
int marks = 0;
};
bool operator<(const Student& a, const Student& b) {
if (a.marks != b.marks) return a.marks < b.marks;
return a.name < b.name;
}
int main() {
std::vector<Student> list = {{"Ravi", 72}, {"Ada", 90}, {"Mei", 72}};
std::sort(list.begin(), list.end());
for (const Student& s : list) {
std::cout << s.name << " " << s.marks << "\n";
}
return 0;
}Output
Mei 72 Ravi 72 Ada 90
4.1.4 Assignment operators
The compiler generates a copy assignment operator that assigns each member. For a class that owns a resource, that default can copy a pointer and leave two objects owning the same memory. A user-defined operator= should release the old resource, copy the new state, and return *this.
Guard against self-assignment when the operation would destroy the source before reading it. if (this == &other) return *this; is the usual check. A class that needs a destructor, a copy constructor, and a copy assignment operator usually needs all three, a guideline known as the rule of three.
4.1.5 Guidelines
Overload an operator when the symbol has an obvious meaning for the type. Vector addition, string concatenation, and stream output all fit. Avoid overloading &&, ||, and the comma operator, because the overload loses short-circuit behavior or the built-in sequencing that callers expect.
Keep related operators consistent. If a == b is true, a != b should be false. If a < b is true, b < a should be false. A conversion operator should usually be explicit so the compiler does not convert a type in surprising places.
4.1.6 Common mistakes
- Defining
operator+as a member when the left operand is a stream. - Returning
voidfromoperator<<, which stops chained output. - Returning a value from
operator+=instead of a reference. - Omitting a field from
operator<, which makes two different objects compare equal. - Writing an assignment operator that leaks the old resource or fails on self-assignment.
- Overloading an operator with a meaning that differs from the built-in one.
4.1.7 Practice
- Add
operator-toVec2and print the result. - Write
operator<<for aCounterand use it in a chained output statement. - Add
operator==andoperator!=forStudentso both compare marks and names. - Sort a vector of
Studentby name instead of marks. - Explain why
operator<<is a free function rather than a member ofVec2.
