Operator Overloading

Give a user-defined type its own meaning for +, ==, [], (), <<, and other built-in operators, so objects can be used with the same natural syntax as the built-in types.

C++ lets a class define what an operator means when applied to its own type. Operator overloading does not invent new operators or change their precedence or associativity — it gives an existing operator symbol, applied to a user-defined type, a meaning the class author writes as an ordinary function. Used well, this lets a Vector2D add with +, a Fraction compare with ==, and a Matrix be indexed with [] just as naturally as the built-in types support those operators.

What Is Operator Overloading?

Operator overloading is the ability to define what an operator such as +, ==, <<, or [] does when one or more of its operands is a user-defined type — a class or enum. The compiler translates operator usage into a call to a function named operator followed by the operator's symbol.

The expression a + b is translated by the compiler into a call to whichever operator+ the class provides.

Point a{1, 2};
Point b{3, 4};
Point c = a + b;   // calls operator+(a, b), or a.operator+(b)

Why Operators Can Be Overloaded

Without operator overloading, every operation on a user-defined type would need a named function — add(a, b), equals(a, b), print(a) — even for operations that have an obvious, conventional symbol. Operator overloading lets a type participate in expressions using the same notation programmers already use for built-in types, which keeps code that manipulates the type readable and consistent with the rest of the language.

This is purely a notational and usability feature: anything expressible with an overloaded operator could also be written as an ordinary named member function. The value is in matching the syntax a reader already expects for that kind of operation.

Basic Syntax

An operator function is declared with the keyword operator followed by the operator symbol, taking the place of an ordinary function name.

operator+ is declared like an ordinary member function; C++ arranges for a + b to call it as a.operator+(b).

class Point
{
public:
    Point(int x, int y) : x(x), y(y) {}

    Point operator+(const Point& other) const
    {
        return Point(x + other.x, y + other.y);
    }

private:
    int x;
    int y;
};

Member vs. Non-Member Operators

An operator can be defined either as a member function of the class or as a free (non-member) function, usually taking its operands by const reference.

Both forms produce the same call syntax a + b at the point of use; they differ only in how the operator function itself is declared and where its operands come from.

As a member function, the left operand is the implicit *this and only the right operand appears as a parameter. As a non-member function, both operands are ordinary parameters.

// Member version: a + b becomes a.operator+(b)
class Point
{
public:
    Point operator+(const Point& other) const;
};

// Non-member version: a + b becomes operator+(a, b)
Point operator+(const Point& lhs, const Point& rhs);

Choosing Member vs. Non-Member

Some operators are required to be members: assignment (operator=), the subscript operator (operator[]), the function-call operator (operator()), and the member-access operator (operator->) must be non-static member functions. Most other operators can be written either way.

A common guideline: prefer a member function when the operator needs to access the class's private state and the left operand is naturally 'the object itself' (as with compound assignment operators such as operator+=). Prefer a non-member function — often a friend — when the operator should treat both operands symmetrically, particularly when implicit conversions to the class type should be allowed on either side.

Unary Operators

A unary operator overload takes no explicit parameter as a member (the single operand is *this) or one parameter as a non-member function.

operator- here negates both coordinates; operator! is repurposed to test whether the point is at the origin — the class author chooses the meaning, but it should stay close to the operator's conventional intent.

class Point
{
public:
    Point(int x, int y) : x(x), y(y) {}

    Point operator-() const   // unary minus: negate
    {
        return Point(-x, -y);
    }

    bool operator!() const    // logical not: "is origin"
    {
        return x == 0 && y == 0;
    }

private:
    int x;
    int y;
};

Binary Operators

A binary operator overload takes one explicit parameter as a member function (the right operand; the left operand is *this) or two parameters as a non-member function.

A non-member binary operator+ taking both operands explicitly; it would typically be declared a friend if it needs direct access to private members instead of going through accessors.

Point operator+(const Point& lhs, const Point& rhs)
{
    return Point(lhs.getX() + rhs.getX(), lhs.getY() + rhs.getY());
}

Arithmetic Operators

Arithmetic operators such as +, -, *, and / are usually implemented in terms of their compound-assignment counterparts (+=, -=, *=, /=): the compound-assignment member modifies *this directly, and the plain binary operator is written as a non-member function that makes a copy and delegates to it.

operator+ takes its left operand by value (a working copy), adds rhs into it using operator+=, and returns the copy — this avoids duplicating the addition logic in two places.

class Point
{
public:
    Point(int x, int y) : x(x), y(y) {}

    Point& operator+=(const Point& other)
    {
        x += other.x;
        y += other.y;
        return *this;
    }

    int getX() const { return x; }
    int getY() const { return y; }

private:
    int x;
    int y;
};

Point operator+(Point lhs, const Point& rhs)
{
    lhs += rhs;   // delegate to the compound-assignment member
    return lhs;
}

Comparison Operators

Equality (==) and inequality (!=) compare two objects for logical equivalence; ordering operators (<, >, <=, >=) impose an ordering. Only the operators a type's usage actually needs should be defined.

operator!= is implemented in terms of operator== to avoid duplicating the equality logic; operator< defines a consistent ordering (here, lexicographic by x then y).

C++20 introduces the three-way comparison operator (operator<=>), which can generate several of the relational operators automatically from a single definition. That feature belongs to the C++20 Features topic; the classic per-operator style shown here remains valid and widely used in earlier standards.

bool operator==(const Point& lhs, const Point& rhs)
{
    return lhs.getX() == rhs.getX() && lhs.getY() == rhs.getY();
}

bool operator!=(const Point& lhs, const Point& rhs)
{
    return !(lhs == rhs);
}

bool operator<(const Point& lhs, const Point& rhs)
{
    if (lhs.getX() != rhs.getX())
    {
        return lhs.getX() < rhs.getX();
    }
    return lhs.getY() < rhs.getY();
}

Stream Insertion and Extraction Operators

operator<< (stream insertion, for output) and operator>> (stream extraction, for input) must be non-member functions when used with a class, because the left operand is a standard stream type such as std::ostream, not the class itself — a member function's left operand is always the class's own type.

operator<< returns the stream by reference so calls can be chained (std::cout << p << '\n';), following the same convention std::ostream itself uses.

#include <iostream>

class Point
{
public:
    Point(int x, int y) : x(x), y(y) {}
    int getX() const { return x; }
    int getY() const { return y; }

private:
    int x;
    int y;
};

std::ostream& operator<<(std::ostream& out, const Point& p)
{
    out << '(' << p.getX() << ", " << p.getY() << ')';
    return out;
}

int main()
{
    Point p(3, 4);
    std::cout << p << '\n';
    return 0;
}

Function Call Operator

operator() lets an object be called as if it were a function, producing what is often called a function object or functor. Unlike most operators, it can take any number of parameters of any types, since the language does not treat operator() as unary or binary.

triple is an ordinary object that can be invoked like a function because it defines operator(); result is 21.

class Multiplier
{
public:
    explicit Multiplier(int factor) : factor(factor) {}

    int operator()(int value) const
    {
        return value * factor;
    }

private:
    int factor;
};

Multiplier triple(3);
int result = triple(7);   // calls triple.operator()(7)

Subscript Operator

operator[] provides array-style indexing. It is a member-only operator and conventionally has both a const and non-const overload, so indexing a const object returns a read-only view while indexing a non-const object allows modification.

Both overloads return a reference to the element rather than a copy, so arr[0] = 10; writes directly into the underlying storage.

Before C++23, operator[] accepts exactly one parameter; a multi-argument subscript (matrix[i, j]) requires either operator()(i, j) or C++23's multi-parameter operator[], which is outside this topic's C++17 baseline.

class IntArray
{
public:
    explicit IntArray(std::size_t n) : data(n, 0) {}

    int& operator[](std::size_t index)
    {
        return data[index];
    }

    const int& operator[](std::size_t index) const
    {
        return data[index];
    }

private:
    std::vector<int> data;
};

IntArray arr(5);
arr[0] = 10;              // non-const overload: allows modification
const IntArray& view = arr;
int x = view[0];          // const overload: read-only access

Increment and Decrement Operators

Both prefix (++x) and postfix (x++) forms can be overloaded. They are distinguished by an unused dummy int parameter on the postfix version — a purely syntactic marker with no meaning of its own.

Prefix returns a reference to the already-incremented object (matching how ++x behaves for built-in types); postfix saves the pre-increment value, increments the object, and returns the saved copy — matching how x++ evaluates to the old value.

class Counter
{
public:
    Counter& operator++()          // prefix: ++c
    {
        ++value;
        return *this;
    }

    Counter operator++(int)        // postfix: c++
    {
        Counter temp = *this;
        ++value;
        return temp;
    }

    int get() const { return value; }

private:
    int value = 0;
};

Return Types

An operator's return type should generally match what the corresponding built-in operator returns for similar types. Assignment and compound-assignment operators (=, +=, -=, and similarly ++ prefix) conventionally return a reference to *this, so chained expressions like a = b = c work. Arithmetic operators that produce a new value (+, -, *) conventionally return by value, since the result is a new object distinct from either operand.

operator+= modifies and returns the existing object; operator+ produces and returns a brand-new object, so it cannot return a reference to a local.

Point& operator+=(const Point& other);   // returns *this by reference: chainable
Point  operator+(Point lhs, const Point& rhs);  // returns a new Point by value

Const Correctness

An operator that does not modify the object it is called on should be marked const, exactly like any other read-only member function — this includes comparison operators, most arithmetic operators, and the const overload of operator[]. An operator that does modify the object, such as operator+= or prefix operator++, should not be const.

Marking read-only operators const allows them to be used on const objects and const references, which matters especially for parameters passed by const reference, a very common pattern for operator parameters.

Symmetry Considerations

When an operator should treat both operands the same way — most notably operator+ and the comparison operators — a non-member function is often preferable to a member function, because it allows implicit conversions to the class type on either operand, not only the right-hand one.

Because operator+ is a non-member function taking both operands by value/reference, the implicit Money(double) constructor can convert either side. If operator+ had been a member function, only 5.0 as the right operand would work, since the left operand of a member call must already be a Money.

class Money
{
public:
    Money(double amount) : amount(amount) {}   // implicit conversion from double
    double getAmount() const { return amount; }

private:
    double amount;
};

Money operator+(const Money& lhs, const Money& rhs)
{
    return Money(lhs.getAmount() + rhs.getAmount());
}

Money m(10.0);
Money total1 = m + 5.0;   // 5.0 converts to Money for the right operand
Money total2 = 5.0 + m;   // 5.0 converts to Money for the left operand too

Operators That Cannot Be Overloaded

A small set of operators cannot be given user-defined meaning: the scope resolution operator (::), the member-access operator (.), the pointer-to-member access operator (.*), and the ternary conditional operator (?:). sizeof and typeid are also not overloadable operators in the ordinary sense.

These exclusions mostly exist because the compiler needs a fixed, unambiguous meaning for these operators to perform name lookup and type resolution — allowing user-defined meanings would make the language ambiguous or impossible to parse consistently.

Operator   | Overloadable?
-----------+---------------
::         | No
.          | No
.*         | No
?:         | No
sizeof     | No
typeid     | No
(all other operators listed on this page) | Yes

Fixed Arity and Precedence

Overloading an operator can change what it does, but it cannot change how many operands it takes, its precedence, its associativity, or whether it is left-to-right or right-to-left — a binary + always takes exactly two operands and always binds looser than *, regardless of what class defines it. New operator symbols also cannot be invented; only the existing, fixed set of overloadable operators may be given class-specific meaning.

Overloading Assignment

operator= is itself an overloadable operator, and it is a special member function in its own right — the copy assignment operator (taking a const reference) and move assignment operator (taking an rvalue reference) covered on the Rule of Zero/Three/Five and Copy Semantics pages are both examples of overloading operator=. It must always be a non-static member function; the language does not permit a non-member operator=.

Best Practices

Keep an operator's behavior close to its conventional mathematical or logical meaning — operator+ should perform something recognizably like addition. Reusing an operator symbol for an unrelated purpose makes code that uses it harder to read correctly.

Mark operators const whenever they do not modify the object, so they remain usable on const objects and through const references — a very common way operator parameters are passed.

Implement related operators in terms of one another to keep them consistent: operator!= in terms of operator==, ordering operators consistent with each other, and operator+ in terms of operator+= rather than duplicating logic.

Prefer non-member functions for symmetric binary operators (+, ==, and similar) so implicit conversions apply to either operand, and reserve member functions for operators that must be members (=, [], (), ->) or that naturally modify *this (+=, prefix ++).

Return by value from operators that produce a new object, and by reference to *this from operators that modify and return the existing object, matching how the corresponding built-in operators behave.

Provide both a const and non-const overload of operator[] when the type should support read-only access through a const reference as well as modification through a non-const one.

Complete Example

A Fraction type with a compound-assignment member, a delegating binary operator+, an equality operator using cross-multiplication, and a stream-insertion operator for printing.

a + b calls the non-member operator+, which copies a into lhs and calls lhs += b operator+= computes 1*3 + 1*2 = 5 for the numerator and 2*3 = 6 for the denominator sum becomes 5/6, printed as "1/2 + 1/3 = 5/6" sum == Fraction(5, 6) cross-multiplies 5*6 == 5*6, which is true

#include <iostream>

class Fraction
{
public:
    Fraction(int num, int den) : num(num), den(den) {}

    Fraction& operator+=(const Fraction& other)
    {
        num = num * other.den + other.num * den;
        den = den * other.den;
        return *this;
    }

    int getNum() const { return num; }
    int getDen() const { return den; }

private:
    int num;
    int den;
};

Fraction operator+(Fraction lhs, const Fraction& rhs)
{
    lhs += rhs;
    return lhs;
}

bool operator==(const Fraction& lhs, const Fraction& rhs)
{
    return lhs.getNum() * rhs.getDen() == rhs.getNum() * lhs.getDen();
}

std::ostream& operator<<(std::ostream& out, const Fraction& f)
{
    out << f.getNum() << '/' << f.getDen();
    return out;
}

int main()
{
    Fraction a(1, 2);
    Fraction b(1, 3);

    Fraction sum = a + b;
    std::cout << a << " + " << b << " = " << sum << '\n';

    std::cout << std::boolalpha << (sum == Fraction(5, 6)) << '\n';
    return 0;
}

Mental Model

Operator overloading is purely a translation from operator syntax to a function call the class defines; the grammar and precedence of the operator itself is fixed by the language, not by the class.

a + b
   |
   | compiler rewrites this expression as a function call
   v
member form:     a.operator+(b)
non-member form: operator+(a, b)

The SYMBOL, PRECEDENCE, and ARITY of + never change.
Only what happens when it is CALLED changes — because
operator+ is, underneath, an ordinary function you wrote.

Quick Reference

A compact summary of which form each operator category conventionally takes and what it conventionally returns.

Operator category      | Typical form        | Conventional return
-------------------------+-----------------------+----------------------
Arithmetic (+, -, *, /)  | non-member, by value  | new object, by value
Compound assign (+=)     | member                | reference to *this
Comparison (==, <, ...)  | non-member, const     | bool
Stream (<<, >>)          | non-member (required) | reference to the stream
Subscript ([])            | member (required)     | reference to element
Function call (())        | member (required)     | whatever the call produces
Assignment (=)             | member (required)     | reference to *this
Prefix ++/--                | member                | reference to *this
Postfix ++/--                | member, with int tag  | old value, by value

Key Takeaways

Operator overloading translates operator syntax (a + b) into a function call (operator+(a, b) or a.operator+(b)) that a class defines — it never changes an operator's arity, precedence, or associativity.

Assignment, subscript, function-call, and member-access operators must be non-static member functions; most other operators can be members or non-members.

Symmetric binary operators like + and == are usually best as non-member functions so implicit conversions apply to either operand.

Arithmetic operators are conventionally written in terms of their compound-assignment counterpart to avoid duplicated logic.

Operators that do not modify the object should be marked const; operators that produce a new value should return by value, while assignment-like operators return a reference to *this.

A fixed handful of operators — ::, ., .*, and ?: among them — cannot be overloaded at all.

Given a class Point with a member function Point operator+(const Point& other) const, what function call does the expression a + b translate into?

It translates into a.operator+(b) — the left operand becomes the implicit object (*this) and the right operand becomes the explicit parameter.

Why must operator<< for a class be defined as a non-member function rather than a member function of that class?

A member operator's left operand must already be an object of the class defining it (it becomes *this). But in std::cout << obj;, the left operand is a std::ostream, not the class — the class does not own std::ostream and cannot add a member function to it. A non-member function operator<<(std::ostream&, const MyClass&) can take the stream as its first parameter instead.

Why is it conventional to implement operator+ in terms of operator+= rather than writing its addition logic independently?

It avoids duplicating the same computation in two places. operator+= already knows how to combine two objects of the type and modify one in place; operator+ can simply copy its left operand, delegate to operator+=, and return the copy — keeping the addition logic defined exactly once.

A class defines both a non-const int& operator[](std::size_t) and a const int& operator[](std::size_t) const overload. Which one is selected when indexing a const reference to the object, and why does this matter for correctness?

The const overload is selected, because operator[] can only be called on a const object through a const-qualified member function. This matters because the const overload returns a const reference, preventing modification through a const view of the object — while the non-const overload, selected for non-const objects, returns a modifiable reference.

List two operators that cannot be overloaded in C++, and give the practical reason overloading them is disallowed.

Examples include the scope resolution operator (::) and the member-access operator (.), along with .* and the ternary operator ?:. These are excluded because the compiler needs a fixed, unambiguous meaning for them to perform basic name lookup and expression parsing — allowing a class to redefine what a.b means, for instance, would make member access ambiguous.

Trace through this code. What does each of d.get(), e.get(), and c.get() print, and why do d and e differ?

c starts at 5. c++ (postfix) saves a copy of c (value 5) into temp, increments c to 6, and returns temp — so d ends up holding the pre-increment value 5. ++c (prefix) increments c to 7 and returns a reference to c itself, so e is a copy of c after the increment, holding 7. c itself is now 7. Output: 5 7 7.

Write a class Vector2D with double x and y members, a constructor taking both, a non-member operator+ that adds two Vector2D values component-wise, a non-member operator== that compares both components for equality, and a non-member operator<< that prints the vector as (x, y). Demonstrate adding two vectors and printing the result.

operator+ computes (1.0 + 3.0, 2.0 + 4.0), producing Vector2D(4.0, 6.0). operator<< then prints it as (4, 6). The program outputs (4, 6).

What does this print, and what mechanism lets doubleIt(5) be written as if doubleIt were a function?

doubleIt(5) calls doubleIt.operator()(5), returning 5 * 2 = 10. tripleIt(doubleIt(5)) then calls tripleIt.operator()(10), returning 10 * 3 = 30. Output: 10 30. The mechanism is the function-call operator, operator(): because Scaler defines it, an object of type Scaler can be invoked with call syntax just like an ordinary function, making it a function object (functor).

Write a class Money that stores an integer number of cents, with a constructor taking dollars (double, converted to cents), a non-member operator+ and operator< comparing two Money values, and a non-member operator<< that prints the amount as dollars with two decimal places (you may use std::fixed and std::setprecision(2) from <iomanip>). Demonstrate comparing two Money values with <.

a stores 1999 cents, b stores 550 cents. a < b is false (1999 is not less than 550); b < a is true. a + b combines to 2549 cents, reconstructed and printed as 25.49. Output: false true, then 25.49.

class Counter
{
public:
    Counter& operator++()
    {
        ++value;
        return *this;
    }

    Counter operator++(int)
    {
        Counter temp = *this;
        ++value;
        return temp;
    }

    int get() const { return value; }

private:
    int value = 5;
};

int main()
{
    Counter c;
    Counter d = c++;
    Counter e = ++c;
    std::cout << d.get() << ' ' << e.get() << ' ' << c.get();
    return 0;
}
class Scaler
{
public:
    explicit Scaler(int factor) : factor(factor) {}
    int operator()(int value) const { return value * factor; }
private:
    int factor;
};

int main()
{
    Scaler doubleIt(2);
    Scaler tripleIt(3);
    std::cout << doubleIt(5) << ' ' << tripleIt(doubleIt(5));
    return 0;
}