Menu

C++ Operator Overloading: Custom +, ==, and << Operators

C++ operator overloading lets your own types work with built-in operators like +, ==, and <<. Learn the member vs non-member rules, how to overload comparison and stream operators, and the gotchas around return types and the assignment operator.

This page includes runnable editors - edit, run, and see output instantly.

Making Your Types Feel Built-In

You already know that std::string lets you write a + b to concatenate and cout << s to print. Those aren't special compiler tricks - they're ordinary functions with funny names. Operator overloading is the feature that lets your classes plug into the same syntax, so a Vector2 or Money type can be added, compared, and printed exactly like an int.

The mechanism is simple once you see it: an expression like a + b is shorthand. The compiler rewrites it as a call to a function named operator+ and looks for one that matches the operand types. Define that function for your class and a + b suddenly works. This is really a specialized form of function overloading - the same name-resolution rules from there apply, just with operator-shaped names.

Note the function takes both operands by const&: arithmetic shouldn't modify its inputs, and references avoid copying. It returns a new Vector2 by value - p + q must yield a fresh result without touching p or q, just like 2 + 3 doesn't change 2.

Member vs Non-Member

There are two places to define an operator: as a member of the class or as a free (non-member) function. As a member, the left operand is the implicit this, so a binary operator takes only one explicit parameter:

The const after the parameter list matters: a + b shouldn't modify a, so the member is marked const. Use the member form for operators that are inherently tied to the left operand and don't need conversions on it - +=, [], (), ->, and unary operators like -x or ++x.

The catch with members: the left operand can't be converted. With the member operator+ above, a + 50 works (50 converts to Money for the right side), but 50 + a does not compile - the left operand 50 is an int, and you can't add a member function to int. A non-member operator fixes this because both operands are explicit parameters and both can be converted:

Rule of thumb: make symmetric binary operators (+, ==, *) non-members so conversions work on both sides; make operators that must modify or are bound to the left operand (+=, [], =) members.

Overloading the Stream Operator

The single most common operator to overload is << for printing. You cannot make it a member of your class, because the left operand is a std::ostream (like cout), not your type - and you don't own ostream. So it's always a non-member taking the stream by non-const reference and returning it:

Two details make this work. The stream is passed and returned by reference (ostream&) - streams can't be copied, and returning the same stream is what lets you chain cout << "p = " << p << "\n". Each << returns the stream so the next << has something to bind to. Forget the return os; and chaining breaks.

Comparison Operators

To compare your objects with ==, <, and friends, overload the comparison operators. Before C++20 you wrote each one by hand; the key gotcha is that operator< must return a bool and define a consistent ordering:

Writing all six comparisons (==, !=, <, <=, >, >=) by hand is tedious and error-prone. C++20 added the three-way comparison operator <=> (the "spaceship"). Defaulting it plus == generates every comparison for you:

= default tells the compiler to compare members in declaration order, which is exactly the lexicographic ordering you'd write by hand. Prefer this on modern compilers.

The Assignment Operator and Its Traps

operator= (copy assignment) is special: the compiler generates one for you, and for simple classes that default is correct. You only need to write your own when your class manages a resource - raw memory, a file handle - where a member-by-member copy would be wrong. The canonical signature returns *this by reference so assignments can chain (a = b = c):

Two traps live in this short function. First, the self-assignment check if (this == &other): without it, a = a would delete[] data and then read from the just-freed other.data - undefined behavior. Second, the order matters - in a hand-rolled version you must not delete the old buffer before you've safely copied the new one (a real implementation often allocates first, or uses the copy-and-swap idiom, so a failed allocation leaves the object intact).

A broader gotcha: don't overload operators in surprising ways. operator+ that secretly modifies its left operand, or operator== that isn't symmetric, will confuse every reader and break standard-library code that assumes the usual meanings. Overload operators only when the operation is genuinely "addition-like" or "equality-like" for your type.

Next: Access Specifiers

Notice how every example kept its data members private and exposed behavior through a small public surface - constructors, operators, and a few methods. That boundary between what's visible to the outside world and what's hidden inside the class is controlled by access specifiers: public, private, and protected. Next we'll look at exactly what each one allows, why private data with public methods is the default for good encapsulation, and how protected fits into inheritance.

Frequently Asked Questions

What is operator overloading in C++?

Operator overloading lets you define what built-in operators like +, ==, or << mean for your own types. You write a specially named function - operator+, operator==, etc. - and the compiler calls it whenever the operator appears with operands of your class. It's how string + string concatenates and cout << obj prints a custom object.

Should operators be member functions or non-member (friend) functions in C++?

Use a member function when the left operand is your own class and won't need conversions (e.g. +=, [], ()). Use a non-member function (often a friend) when the left operand might be a built-in type or when you want symmetric conversions on both sides - this is required for operator<< because the left operand is a std::ostream, not your class.

Which C++ operators cannot be overloaded?

You cannot overload :: (scope resolution), . (member access), .* (member-pointer access), ?: (ternary), and sizeof. You also can't invent brand-new operators or change an operator's arity or precedence - + is always binary with the same precedence whether it adds ints or your Vector2.

Coddy programming languages illustration

Learn to code with Coddy

GET STARTED