What Is a Member Function Operator Overload?

Writing an operator as a member means declaring it inside whichever class supplies the value on the left of the symbol. What separates this from the non-member forms is what happens to that value: it never appears in the parameter list. It arrives as the object the function was called on, reachable through *this. Whatever sits to the right of the symbol becomes an ordinary parameter, exactly as it would in any other member function.

Piece of the expression Non-member overload Member overload
Left operand first parameter the implicit object, *this
Right operand second parameter the one and only parameter
Where the function lives namespace scope inside the left operand's class
Access to private data only when declared a friend always
Parameter count for a binary operator two one

That one relocation is responsible for everything else in this lesson: which operators the language insists you write this way, which ones it forbids you from writing this way, and which ones read better one way than the other.

Related Content
The other two forms have lessons of their own: overloading the arithmetic operators using friend functions, and overloading operators using normal functions.

The Same Operator, Written Both Ways

Here is a Cargo class whose objects can have kilos added to them. First the non-member version, granted friend status so it can read m_kilos directly:

#include <iostream>

class Cargo
{
private:
    int m_kilos{};

public:
    explicit Cargo(int kilos)
        : m_kilos{kilos} {}

    friend Cargo operator+(const Cargo& load, int extra);

    int kilos() const { return m_kilos; }
};

// Lives outside Cargo, so both sides are named arguments
Cargo operator+(const Cargo& load, int extra)
{
    return Cargo{load.m_kilos + extra};
}

int main()
{
    const Cargo pallet{45};
    const Cargo restacked{pallet + 12};

    std::cout << "Restacked to " << restacked.kilos() << " kg\n";

    return 0;
}
Restacked to 57 kg

Now the member version. Note that main() is untouched:

#include <iostream>

class Cargo
{
private:
    int m_kilos{};

public:
    explicit Cargo(int kilos)
        : m_kilos{kilos} {}

    Cargo operator+(int extra) const;

    int kilos() const { return m_kilos; }
};

// Declared inside Cargo: left-hand side arrives as *this
Cargo Cargo::operator+(int extra) const
{
    return Cargo{m_kilos + extra};
}

int main()
{
    const Cargo pallet{45};
    const Cargo restacked{pallet + 12};

    std::cout << "Restacked to " << restacked.kilos() << " kg\n";

    return 0;
}
Restacked to 57 kg

Callers cannot tell the difference. pallet + 12 is spelled the same, means the same, and produces the same number. Only the declaration moved.

Where the Left Operand Went

Trace pallet + 12 through both versions and the disappearing parameter stops being mysterious.

The non-member version turns the expression into operator+(pallet, 12). Two arguments, both visible.

The member version turns it into pallet.operator+(12). One visible argument, with pallet promoted to an object prefix. But a member function call always carries a hidden this pointer aimed at the object in front of the dot, so the call still transports two pieces of information into the function body: the address of pallet, and the value 12. The bare m_kilos inside the body is shorthand for this->m_kilos, which is why the left operand does not need naming.

Related Content
The hidden this pointer is covered in the lesson on the hidden "this" pointer and member function chaining.

Moving an Existing Overload Into the Class

Given a working non-member overload, three edits relocate it:

Edit Before After
Declaration friend Cargo operator+(const Cargo&, int); Cargo operator+(int) const;
Definition Cargo operator+(...) at namespace scope Cargo Cargo::operator+(...)
Body load.m_kilos m_kilos

The const on the member declaration is worth a moment. Adding kilos does not change the pallet you started with, it builds a new Cargo, so *this should be read-only. Leave the const off and pallet + 12 will fail to compile the moment pallet itself is const.

Operators the Language Only Accepts as Members

Four symbols accept no other form:

Symbol Name What it is usually for
= assignment replacing one object's contents with another's
[] subscript indexing into a container-like class
() function call making an object callable
-> member selection forwarding member access, as a smart pointer does
Warning
None of the four in that table has a non-member form. Put the overload inside the class, or the build fails. There is nothing to weigh up.

The compiler is blunt about it. This code does not compile:

#include <array>
#include <cstddef>
#include <iostream>

class Rack
{
public:
    std::array<int, 3> m_bays{8, 13, 21};
};

int operator[](const Rack& rack, std::size_t bay)
{
    return rack.m_bays[bay];
}

int main()
{
    const Rack shelving{};

    std::cout << shelving[1] << '\n';

    return 0;
}
s.cpp:11:5: error: 'int operator[](const Rack&, std::size_t)' must be a member function

Operators You Cannot Write as Members

The rule that a member overload lives in the left operand's class cuts the other way too. If you do not own that class, or the left operand is not a class at all, the member form is off the table.

Streaming is the everyday case. For std::cout << pallet, the left operand has type std::ostream, a standard library class nobody is going to reopen and add members to. That forces operator<< out of the class:

#include <iostream>

class Cargo
{
private:
    int m_kilos{};

public:
    explicit Cargo(int kilos)
        : m_kilos{kilos} {}

    friend std::ostream& operator<<(std::ostream& stream, const Cargo& load);
};

std::ostream& operator<<(std::ostream& stream, const Cargo& load)
{
    stream << load.m_kilos << " kg";

    return stream;
}

int main()
{
    const Cargo pallet{45};

    std::cout << pallet << '\n';

    return 0;
}
45 kg

The fundamental types are the other case. Our member operator+ handles pallet + 12, but reverse the operands and the left side is an int. A member overload would have to be declared inside int, and int is not a class, so there is nowhere to put it. This code does not compile:

#include <iostream>

class Cargo
{
private:
    int m_kilos{};

public:
    explicit Cargo(int kilos)
        : m_kilos{kilos} {}

    Cargo operator+(int extra) const
    {
        return Cargo{m_kilos + extra};
    }

    int kilos() const { return m_kilos; }
};

int main()
{
    const Cargo pallet{45};
    auto reversed{12 + pallet};

    std::cout << reversed.kilos() << '\n';

    return 0;
}
s.cpp: In function 'int main()':
s.cpp:23:22: error: no match for 'operator+' (operand types are 'int' and 'const Cargo')

Symmetry: What the Non-Member Form Buys You

Because a non-member overload states both operands as parameters, nothing stops you writing the mirrored pair. Adding operator+(int, const Cargo&) alongside operator+(const Cargo&, int) makes both operand orders legal, and the second can simply defer to the first:

#include <iostream>

class Cargo
{
private:
    int m_kilos{};

public:
    explicit Cargo(int kilos)
        : m_kilos{kilos} {}

    friend Cargo operator+(const Cargo& load, int extra);
    friend Cargo operator+(int extra, const Cargo& load);

    int kilos() const { return m_kilos; }
};

Cargo operator+(const Cargo& load, int extra)
{
    return Cargo{load.m_kilos + extra};
}

Cargo operator+(int extra, const Cargo& load)
{
    return load + extra;
}

int main()
{
    const Cargo pallet{45};

    std::cout << (pallet + 12).kilos() << " kg\n";
    std::cout << (12 + pallet).kilos() << " kg\n";

    return 0;
}
57 kg
57 kg

That is the practical meaning of symmetry: with both operands as parameters, neither one is privileged, so the overload works no matter which type shows up on the left. A member overload can never reach that, because the left slot is permanently reserved for its own class.

When the Left Operand Is the Thing Being Changed

Compound assignment flips the argument around. pallet += 12 is not asking for a new object, it is asking for the existing pallet to be heavier afterwards. The left operand is therefore guaranteed to be a Cargo, it is guaranteed to be modified, and the member form says both of those things without any extra explanation:

#include <iostream>

class Cargo
{
private:
    int m_kilos{};

public:
    explicit Cargo(int kilos)
        : m_kilos{kilos} {}

    Cargo& operator+=(int extra)
    {
        m_kilos += extra;

        return *this;
    }

    int kilos() const { return m_kilos; }
};

int main()
{
    Cargo pallet{45};
    pallet += 12;

    std::cout << "Pallet weighs " << pallet.kilos() << " kg\n";

    return 0;
}
Pallet weighs 57 kg

Note what is missing: no const on the member, because the object is being written to, and a reference return so the modified object can be used again. Anyone reading the signature can see immediately that *this is the target and extra is merely consulted.

Unary operators land in the same camp for a different reason. A unary operator has exactly one operand, that operand is *this, and so the member version takes no parameters at all. Nothing to declare beats one parameter to declare.

Choosing a Form

Once the forced cases are out of the way, the shape of the operator decides for you.

Operator you are overloading Form to use Reason
=, [], (), -> member the language rejects anything else
unary, such as -x or !x member the sole operand is already *this, so the member needs no parameters
binary that leaves the left operand alone, such as a + b normal function, or friend if it needs private access both operands stay explicit, so any type may appear on the left
binary that modifies the left operand and you own that class, such as a += b member the object being written to is *this, and the reader can see which side is which
binary that modifies the left operand of a class you do not own, such as stream << a normal function, or friend if it needs private access you cannot add members to somebody else's class
Best Practice
One question settles most cases: after the operation, has the thing on the left changed? Answer yes, and put the overload inside that class, assuming the class is yours to edit. Answer no, and keep both sides as explicit parameters in a normal function, promoting it to a friend only when the body genuinely needs private data.

Key Terminology

  • Member function operator overload: an operator overload declared inside a class, where the left operand becomes the implicit *this object and every remaining operand becomes a parameter.
  • Implicit object: the object a member function was called on, passed invisibly through the this pointer instead of appearing in the parameter list.
  • Symmetry: the property of a non-member overload that both operands are ordinary parameters, so neither operand type is privileged over the other.

Looking Forward

Three of the four member-only operators get lessons of their own later in this chapter: overloading the subscript operator, overloading the parenthesis operator, and overloading the assignment operator. Overloading unary operators shows the parameterless member form in action, and overloading typecasts covers conversion operators, which are also members-only.

Summary

  • Writing an operator as a member puts it inside the class on the left of the symbol, and that value arrives through *this instead of the parameter list.
  • Every other operand becomes a normal parameter, so a binary operator written as a member takes one parameter instead of two.
  • Relocating an existing overload takes three edits: declare it inside the class, qualify the definition with ClassName::, and drop the now-redundant references to the old left parameter.
  • =, [], (), and -> must be members. The compiler reports anything else as an error.
  • The member form is unavailable whenever the left side is a fundamental type such as int, or belongs to code you cannot edit such as std::ostream. Fall back to a normal function there, promoting to friend only for private access.
  • Non-member overloads are symmetric: both operands are parameters, so you can supply overloads for either operand order.
  • If the operation leaves its left side untouched, a normal function reads better; if the operation rewrites its left side, a member reads better.
  • Unary operators are conventionally members because the member form has no parameters at all.