Appearance
Operator Overloading
Operator overloading lets your types act like built-ins where it's intuitive. Use it sparingly and only when semantics are obvious.
Quick example
We'll create a Point class and overload the + operator to add two points.
cpp
class Point {
int x {0}, y {0};
public:
Point() = default;
Point(int xv, int yv) : x(xv), y(yv) {}
// Add two points; return a new Point
Point operator+(const Point& rhs) const {
return Point(x + rhs.x, y + rhs.y);
}
};
// Usage
Point a{1, 2}, b{3, 4};
Point c = a + b; // (4, 6)You can do similar overloads for other operators, like -, *, /, ==, !=, <, >, <=, >=, [], =, (), <<, >>, ++, --, etc.
When should you overload?
- Use only if the meaning matches expectations (e.g., add, compare, index).
- Prefer clear method names if operator intent isn't obvious.
Overloads can also be placed outside the class, as non-members. For example:
cpp
class Point {
int x {0}, y {0};
public:
Point() = default;
Point(int xv, int yv) : x(xv), y(yv) {}
friend Point operator+(const Point& lhs, const Point& rhs);
};
friend Point operator+(const Point& lhs, const Point& rhs)
{
return Point(lhs.x + rhs.x, lhs.y + rhs.y);
}
// Usage
Point a{1, 2}, b{3, 4};
Point c = a + b; // (4, 6)Member vs non-member (outside) overloads
- Make it a member when:
- It modifies the left operand (e.g., operator+=, operator[], operator=, operator++/--).
- It needs access to this' internals and is inherently asymmetric.
- Make it a non-member (often friend) when:
- It's symmetric w.r.t. operands (e.g., operator+, operator==, operator<<).
- You want implicit conversions on both sides (e.g., scalar + Vector).
One possible use case of defining a non-member overload is when you want to add two types that are convertible to each other in an inheritance hierarchy.
cpp
class Base {
int x {0};
public:
Base() = default;
Base(int xv) : x(xv) {}
};
class Derived : public Base {
int y {0};
Derived() = default;
Derived(int xv) : Base(xv) {}
friend Derived operator+(const Base& lhs, const Derived& rhs);
};
friend Derived operator+(const Base& lhs, const Derived& rhs)
{
return Derived(lhs.x + rhs.x, rhs.y);
}
// Usage
Base a{1}, b{2, 3};
Derived c = a + b; // (3, 3)The above works because Base is implicitly convertible to Derived. If operator+ were a member, it would not be possible to add a Base to a Derived.
Pre-increment vs post-increment
Overloading the increment operators requires special syntax because they look the same. Pre-increment is overloaded as usual - by taking no arguments and returning a reference to the object. Post-increment is overloaded by taking an unused int argument and returning a copy of the old object.
cpp
class Counter {
int value {0};
public:
// pre-increment: ++c → modify, then return lvalue reference
Counter& operator++() {
++value;
return *this;
}
// post-increment: c++ → return old value (by value), then modify
Counter operator++(int) {
Counter old = *this; // copy
++(*this);
return old;
}
};- Pre-increment returns Counter& and is usually faster (no copy).
- Post-increment returns a temporary (old state) and cannot be chained for mutation.
Common overload patterns
Value + compound ops (member for +=, non-member for +)
cpp
class Vector2D {
double x {0}, y {0};
public:
Vector2D() = default;
Vector2D(double xv, double yv) : x(xv), y(yv) {}
Vector2D& operator+=(const Vector2D& rhs) {
x += rhs.x; y += rhs.y; return *this;
}
};
// Non-member to keep symmetry and allow implicit conversions on both sides
inline Vector2D operator+(Vector2D lhs, const Vector2D& rhs) {
lhs += rhs; // reuse compound logic
return lhs; // NRVO/move
}Equality (non-member) and stream output (non-member)
cpp
class P {
int x {0}, y {0};
public:
P() = default;
P(int xv, int yv) : x(xv), y(yv) {}
friend bool operator==(const P& a, const P& b) {
return a.x == b.x && a.y == b.y;
}
friend bool operator!=(const P& a, const P& b) { return !(a == b); }
friend std::ostream& operator<<(std::ostream& os, const P& p) {
return os << '(' << p.x << ',' << p.y << ')';
}
};Subscript (member) with const/non-const overloads
cpp
class SmallArray {
int data[4] {0,0,0,0};
public:
int& operator(std::size_t i) { return data[i]; }
const int& operator(std::size_t i) const { return data[i]; }
};Quick rules of thumb
- Implement compound ops (+=, -=, *=, /=) as members; build binary ops (+, -, *, /) from them as non-members.
- Comparison operators are typically non-members; ensure consistency (== and != align; < implies others).
- operator[] and operator= must be members.
- Stream operators << and >> should be non-members.
- Mark non-mutating operators as const; return references for compound ops.
- Prefer explicit conversion operators to avoid surprises.
Pitfalls to avoid
- Inconsistent semantics (e.g., == and != don't agree).
- Returning by value where reference is required (compound ops), or vice versa.
- Overloading when a named function would be clearer.
- Forgetting const-correctness; breaking symmetry by making only one side convertible.
Questions
Q: What is the main purpose of operator overloading in C++?
Operator overloading allows you to define custom behavior for C++ operators when they are used with your own classes. This enables you to use operators like +, -, *, /, ==, etc. with your custom types, making the code more intuitive and readable.
Q: Which of the following operators CANNOT be overloaded?
The scope resolution operator (:😃, member access operator (.), and conditional operator (?😃 cannot be overloaded. These operators have fundamental language semantics that cannot be changed. All other operators like arithmetic, comparison, assignment, and function call operators can be overloaded.
Q: What is the difference between member and non-member operator overloading?
Member operators have the left operand as 'this' and can access private members, while non-member operators have both operands as parameters and cannot access private members directly. Non-member operators are often implemented as friend functions to access private members.
Q: When should you use operator overloading?
Operator overloading should be used when the operator behavior is intuitive and follows mathematical conventions. For example, using + for addition, == for equality, and < for comparison. Overloading operators with non-intuitive behavior can make code confusing and harder to maintain.
Q: What is the purpose of the 'explicit' keyword with conversion operators?
The 'explicit' keyword with conversion operators prevents implicit conversions and requires explicit casting. This helps avoid unexpected type conversions that could lead to bugs or confusing behavior. It's similar to using 'explicit' with single-parameter constructors.