Member Operator Overloading
Define operators as class members with implicit this pointer access.
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.
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.
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 |
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 |
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
*thisobject and every remaining operand becomes a parameter. - Implicit object: the object a member function was called on, passed invisibly through the
thispointer 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
*thisinstead 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 asstd::ostream. Fall back to a normal function there, promoting tofriendonly 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.
How did you find this lesson?
Your rating helps us improve the content.
Create an account to track your progress and access interactive exercises. Already have one? Sign in.
Member Operator Overloading - Quiz
Test your understanding of the lesson.
Practice Exercises
Operators as Member Functions
Implement operator overloading using member functions. Learn which operators must be member functions and how to access private data directly.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!