Why your types can use the same symbols
Operator overloading is why std::string + std::string concatenates and why std::cout << 42 prints. The standard library is built on it, and unit 8 would be unusable without it.
It exists so that user-defined types can join the same expression syntax as built-in ones. Without it, math-heavy code degenerates into nested calls like add(mul(a, b), c), where the reader has to unwrap the parentheses to recover the formula.
Operators are functions
In C++, a + b on your own type can be made to work by defining a function named operator+. This is operator overloading, the same overloading idea from lesson 4-3 applied to symbols:
struct Vec2 { double x, y; Vec2(double px, double py) : x(px), y(py) {} Vec2 operator+(const Vec2& rhs) const { return Vec2(x + rhs.x, y + rhs.y); } bool operator==(const Vec2& rhs) const { return x == rhs.x && y == rhs.y; } }; Vec2 c = a + b; // calls a.operator+(b)
Note that the parameter is a const Vec2&, which is lesson 4-2's cheap read-only pass, and that the method itself is const, following lesson 7-2. The pieces you have already learned compose here.
Printing your type: operator<<
std::cout << v needs an operator whose left side is the stream, so it is written as a free function outside the class:
std::ostream& operator<<(std::ostream& os, const Vec2& v) { return os << "(" << v.x << ", " << v.y << ")"; }
Returning the stream is what lets << chain, since the result of the first << becomes the left operand of the next.
Overload only when the meaning is obvious, which in practice means math types and comparisons. An operator+ that launches missiles compiles perfectly and gets you thrown out of code review.
A type that adds and prints like a built-in
Vec2 with both operators in place, so an expression can mix them freely.
#include <iostream> struct Vec2 { double x, y; Vec2(double px, double py) : x(px), y(py) {} Vec2 operator+(const Vec2& rhs) const { return Vec2(x + rhs.x, y + rhs.y); } }; std::ostream& operator<<(std::ostream& os, const Vec2& v) { return os << "(" << v.x << ", " << v.y << ")"; } int main() { Vec2 a(1.0, 2.0), b(3.0, 4.0); std::cout << a + b << "\n"; return 0; }
Output
(4, 6)
The single line std::cout << a + b << "\n"; triggers three separate operator calls. a + b runs first, because + binds tighter than <<, then the stream operator prints the result, and then the chained << prints the newline.
operator+ returns a new Vec2 by value rather than modifying either operand, which is what + should always do. Users expect a + b to leave a and b alone, and the const on the method enforces exactly that for the left operand.
Why operator<< is a free function
In std::cout << v, the left operand is a std::ostream, and a method always takes its own class as the left operand. So a method version would be called as v.operator<<(cout), meaning v << cout, which is backwards from the idiom every C++ programmer expects.
The correct left operand belongs to std::ostream, and you cannot add methods to a standard library class. That leaves a free function as the natural home, taking the stream on the left and your type on the right:
std::ostream& operator<<(std::ostream& os, const Vec2& v);
The stream parameter is a non-const reference, since printing genuinely modifies the stream, while your own type is a const& because printing should not modify it. The return type is also a reference to the same stream, which is what makes chaining work.
This rule generalizes. Any binary operator whose left operand is a type you do not own has to be a free function, which is also true of things like 2.0 * v for scalar multiplication.
Adding subtraction and equality
Two more operators on Vec2, following the same shapes as operator+.
#include <iostream> struct Vec2 { double x, y; Vec2(double px, double py) : x(px), y(py) {} Vec2 operator-(const Vec2& rhs) const { return Vec2(x - rhs.x, y - rhs.y); } bool operator==(const Vec2& rhs) const { return x == rhs.x && y == rhs.y; } }; std::ostream& operator<<(std::ostream& os, const Vec2& v) { return os << "(" << v.x << ", " << v.y << ")"; } int main() { Vec2 a(5.0, 6.0), b(3.0, 4.0); std::cout << a - b << "\n"; std::cout << std::boolalpha << "equal: " << (a == b) << "\n"; return 0; }
Output
(2, 2) equal: false
The two return types differ for good reason. operator- gives back a Vec2 because subtracting vectors yields a vector, while operator== gives back a bool because comparing them yields an answer, not a vector.
Order matters for subtraction in a way it does not for addition, so a - b and b - a give different results and the method must subtract rhs from the members rather than the reverse. Getting that backwards is a classic silent bug, since the code still compiles and still returns a plausible vector.
The parentheses in (a == b) are required. Without them, << binds tighter than == and the compiler would try to compare a stream against a Vec2, producing a wall of errors.
One real caveat: comparing double members with == is exact, and floating-point arithmetic rarely lands on exact values, so a Vec2 computed two different ways can compare unequal despite looking identical when printed. Production code compares within a small tolerance instead.