C++, End to End
Operator Overloading
C++ lets you define what operators like +, ==, and << mean for your own types. Used well, this makes user-defined types read as naturally as built-in ones. Used badly, it makes + do something nobody would guess by looking at it. The rule that keeps it in the "used well" category: an overloaded operator should do the thing its symbol obviously suggests, and nothing more surprising than that.
What can and can't be overloaded
Almost every operator can be overloaded: arithmetic (+ - * /), comparison (== != < > <= >=), the stream operators (<< >>), subscript ([]), function-call (()), and more. A handful can't: ., ::, ?:, and sizeof are fixed, you can't change what they mean, and there's no way around it.
Member vs free-function overloads
Some operators can be overloaded either as a member function or a free (non-member) function. Others have a hard requirement. operator<< for stream output is a classic case where it must be a free function: the left-hand operand is std::ostream&, not your type, and a member function's implicit left operand is always the object it's called on, so a member operator<< would need to be a member of std::ostream itself, which you don't own and can't modify.
A general rule of thumb: if the left-hand operand should always be your type (like a + b where a is your class), a member function overload reads naturally, since it's called as a.operator+(b) under the hood. If the left-hand operand is something you don't own, or you want implicit conversions to apply symmetrically to both sides (so 2 + myNumber and myNumber + 2 both work, not just one direction), a free function is the right shape.
Overloading comparisons
Before C++20, you had to write every comparison operator (==, !=, <, >, <=, >=) by hand, even though most of them can be derived mechanically from just one or two of them. C++20's spaceship operator, <=>, lets the compiler generate the rest for you from a single three-way comparison:
= default here tells the compiler to generate the comparison member-by-member, in declaration order, exactly the way you'd write it by hand for a simple aggregate-like type. If you need custom comparison logic (like the fraction cross-multiplication above), you still write operator<=> yourself instead of defaulting it, and the other five comparison operators are still derived from it automatically.
Overloading arithmetic operators consistently
If you overload +, overload += too, and make sure they agree: a += b should do the same thing as a = a + b, just without the extra temporary and copy. Users of your type will reasonably assume this consistency holds, the way it does for every built-in numeric type, and breaking it (having + and += do subtly different things) is exactly the kind of surprising behavior operator overloading should avoid.
Notice operator+ takes its left operand by value (Vector2D lhs), not by reference. That's deliberate: it needs its own copy to mutate with += and return, and taking it by value lets the compiler use move construction when the caller passes a temporary, which is typically as cheap as (or cheaper than) taking a const& and copying inside the function body.
The trap of surprising overloads
Nothing stops you from overloading + on a Logger type to mean "write two log messages", or overloading [] to run arbitrary unrelated logic instead of indexing. The compiler will happily let you. Readers of your code won't expect it, though, because every other type in the language uses + for addition-like combination and [] for indexing-like access. If an operation doesn't map naturally onto an existing operator's obvious meaning, give it a named member function instead. log.combine(other) is clearer than a + that does something a reader would never guess.
Try it yourself: implement operator+= and derive operator+ from it›
Given this Money class with only a constructor and a getter, add operator+= and derive operator+ from it, following the same pattern as the Vector2D example above, so the two stay consistent by construction rather than by careful duplication.
class Money {
long cents; // store as integer cents to avoid floating-point rounding issues
public:
Money(long c) : cents(c) {}
long getCents() const { return cents; }
// Add operator+= and operator+ here
};