Course outline · 0% complete

0/27 lessons0%

Course overview →

Operator Basics: Making Your Types Feel Native

lesson 7-3 · ~10 min · 19/27

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.