AlgoMaster Logo

Operator Overloading Basics

Medium Priority12 min readUpdated June 6, 2026
Listen to this chapter
Unlock Audio

Operator overloading lets you define what operators like +, ==, or << mean when applied to your own classes, so a Money or Cart object can be added, compared, or printed using the same syntax as int or double. This lesson covers the motivation, the two syntactic forms (member function and non-member function), the operators that cannot be overloaded, and the rules of thumb for keeping overloaded operators predictable. Sibling lessons in this section dig into each operator family in depth, so the examples here stay deliberately small.

Why Operator Overloading Exists

C++ gives built-in types like int and double a rich operator vocabulary. a + b, a == b, or std::cout << a work because the compiler knows what to do. For a user-defined class, the compiler has no idea what + means for two Money objects or whether one Product is "less than" another.

Without operator overloading, every operation on a user-defined type has to be a named function:

That code works, but reading expressions like add(price, add(shipping, tax)) gets noisy fast. The same expression in math notation, price + shipping + tax, is shorter and matches how a person would describe the calculation. Operator overloading bridges that gap. The compiler is taught what + means for Money, and price + shipping + tax compiles into the exact same calls.

The principle is simple: make user-defined types behave like built-in types. If a Money object represents a value, addition and equality should look like addition and equality everywhere else in the language.

What Counts as an Operator

C++ treats most operators as ordinary functions with a special name. The function name is the keyword operator followed by the operator symbol. For example:

Expression you writeFunction the compiler calls
a + boperator+(a, b)
a == boperator==(a, b)
std::cout << aoperator<<(std::cout, a)
cart[0]cart.operator[](0)
++countercounter.operator++()

That table is the whole picture. An expression like a + b is syntactic sugar for a function call. Once that's clear, "overloading an operator" is no different from writing any other function; the function is named operator+ instead of add.

Member Function vs Non-Member Function Form

Operators can be overloaded in two places: as a member function of the class, or as a non-member (free) function that takes the operands as parameters. Both are common, and the choice has real consequences.

Member Function Form

When the operator is a member function, the left-hand operand is the object the function is called on (the implicit this). The right-hand operand becomes the function's parameter.

When the compiler sees price + shipping, it rewrites it as price.operator+(shipping). The const at the end of the function signature is there because adding two Money values shouldn't change either of them.

Non-Member Function Form

The same operator can live outside the class as a free function that takes both operands as parameters:

Same result, different shape. The non-member version takes both operands as parameters, so neither operand is privileged.

Why the Choice Matters

The difference between the two forms shows up when the left operand isn't your class. Consider 100 + price, where 100 is a plain int and price is a Money. The compiler tries to rewrite this as (100).operator+(price), but int has no operator+ that takes Money. A member function on Money can't help, because the member form requires the left operand to be a Money.

A non-member operator+(int, const Money&) solves the problem cleanly, because both operands are parameters. This is why operators where the left operand might be a built-in type (such as << for streams, where the left operand is std::ostream) are almost always written as non-member functions.

The diagram shows what each form constrains. The member form pins the left operand to your class. The non-member form leaves both sides free.

A Quick operator+ Demo on Money

The arithmetic operators lesson in this section covers +, -, *, and / in depth. A short demo here makes the shape concrete and shows how the function signature, the return type, and the call site fit together.

Three details are worth pointing out, because they recur for every binary operator:

  1. Return by value, not by reference. The result of a + b is a brand-new Money. It's not a, it's not b, and it doesn't exist anywhere yet. Returning a reference to a local would be a dangling reference.
  2. The right operand is `const T&`. This avoids copying the right operand and promises not to modify it. For tiny types like Money (which holds a single long), passing by value is also fine, but const T& works for everything and is the standard pattern.
  3. The function is `const`. Adding two Money values shouldn't change the left operand, so the member function is marked const.

A Quick operator== Demo

Equality has a similar shape, but the return type is bool:

The return type matters. operator== returns bool, because that's how equality works for built-in types. Returning an int or a std::string would surprise anyone using the class. Predictability is the point.

Operators You Cannot Overload

Most operators can be overloaded, but a small set is off-limits. The standard reserves these so they always have a single, well-known meaning:

OperatorNameWhy It's Reserved
::Scope resolutionResolves names at compile time, not a runtime operation on values
.Member accessHas to bind to the actual member of the actual object, not a redefinition
.*Member access via pointer-to-memberSame reason as ., just with a pointer-to-member
?:Ternary conditionalRequires lazy evaluation of two branches, which a function call can't replicate
sizeofSize in bytesComputed at compile time from the type itself
typeidRun-time type infoBuilt into the language and RTTI, not a value operation
alignofAlignment requirementCompile-time property of the type, like sizeof
co_awaitCoroutine await (C++20)Has language-level rules for suspending and resuming coroutines

A few of these (?:, sizeof, typeid, alignof) are operators that depend on compiler-level behavior such as short-circuit evaluation or type introspection, which a normal function call cannot reproduce. Others (., .*, ::) would break the language if they could be redefined, because the compiler relies on them to find names and members.

Trying to overload one of these makes the compiler reject the program. For example, with g++:

g++ reports: error: 'operator.' may not be a non-static member function (and similar errors for the others). The exact wording varies, but the rejection is universal.

General Rules of Thumb

Overloading is a tool, not a license. The compiler won't stop + from meaning "subtract", but doing so makes the code unreadable. A handful of rules keep overloaded operators predictable:

Keep the Semantics Aligned With the Built-In Meaning

+ should produce a sum-like result, == should be a real equivalence check (reflexive, symmetric, transitive), < should define a real ordering, << on a stream should write to the stream. An operator+ for Cart that actually removes items invites bugs in every line of code that uses it.

A useful test: would a reader who hasn't seen the class header guess correctly what the operator does, just from the symbol? If the answer is no, name it as a regular function instead.

Return Appropriate Types

Operator familyTypical return typeReason
+, -, *, /New value (by value)Result is a fresh object, not either operand
+=, -=, *=, /=T& (reference to *this)Mirrors built-in compound assignment, supports chaining
==, !=, <, >, <=, >=boolComparisons produce truth values
<<, >> (streams)std::ostream& / std::istream&Supports cout << a << b chaining
= (assignment)T&Same chaining reason: a = b = c
[]T& (and const T& overload)Lets the caller both read and write through the result

The pattern is consistency with built-ins. int a, b, c; a = b = c; works because operator= returns a reference to the assigned-to variable. An overloaded operator= that returns void breaks chaining and surprises users.

Don't Surprise the User

A few specific traps come up repeatedly:

  • Don't overload `&&` or `||` for user types. The built-in versions short-circuit (the right operand isn't evaluated unless needed). Overloaded versions are ordinary functions, so they evaluate both arguments before being called. The behavior change is subtle and almost always wrong.
  • Don't overload `,` (the comma operator). It's legal but rarely useful, and it can hide expensive operations inside what looks like a sequence point.
  • Don't change argument order semantics. If a < b returns true, then b < a should return false when a != b. Anything else breaks std::sort, std::set, and every algorithm that relies on ordering.

Returning a large object by value from operator+ looks expensive, but modern compilers apply Return Value Optimization (RVO) and elide the copy in most cases. Don't return a reference to a local to "save a copy", that's undefined behavior.

Be Honest About const

Operators that don't modify their operands should be marked const (for member functions) or take const references (for non-member functions). This isn't decoration. A const Money can only call const member functions, so forgetting const on operator+ means a const Money can't participate in addition.

When to Choose Member vs Non-Member

A simple decision guide handles 95% of cases:

Walking through the diagram:

  • If the left operand can be something other than your class (a stream, an int, a std::string), the non-member form is required. The classic example is std::cout << product, where the left operand is std::ostream&. Adding a member function to std::ostream isn't possible, so operator<< has to be a free function.
  • If the operator modifies the left operand (assignment, compound assignment, subscripting), use a member function. C++ requires these specific operators (=, [], (), ->) to be member functions anyway, so the language enforces the choice.
  • If the operator is symmetric and doesn't modify operands (+, ==, <), either form works. Many style guides prefer the non-member form for symmetry, because both operands are treated the same way in the function signature. Implementing operator+ as a non-member that uses operator+= internally is a common pattern.

A few operators must be members by language rule: =, [], (), ->. The compiler rejects a non-member version of these. Everything else is a choice guided by the rules above.

A Worked Example: Tying It Together

A small Review class that exercises every piece of this lesson: a non-member operator+ for combining helpfulness scores, a member operator== for comparing reviews, and a const-correct interface. The point isn't to model reviews "correctly", but to show the shape of a class that uses both operator forms cleanly.

Three details in that example:

  1. operator== is a member, marked const, and returns bool. Equality comparisons shouldn't mutate either side, and const correctness lets two const Review objects be compared.
  2. operator+ is a non-member, takes both operands by const reference, and returns a new Review by value. The result is a fresh object, not either input.
  3. The semantics line up with intuition. r1 + r2 produces a "combined" review, and r1 == r2 checks whether two reviews refer to the same product at the same rating. A reader skimming the code would guess correctly what each operator does.

That's the core skill operator overloading teaches: building types that behave like part of the language, not bolted on top of it. The rest of this section drills into each operator family individually.

Quiz

Operator Overloading Quiz

10 quizzes