Overloading Operators Using Member Functions

In the lesson on overloading arithmetic operators with friend functions, you learned one way to overload operators. You also learned operators can be overloaded as normal functions. Many operators can be overloaded a third way: as member functions.

When overloading an operator as a member function:

  • The operator must be added as a member of the left operand's class
  • The left operand becomes the implicit *this object
  • All other operands become function parameters

As a reminder, here's operator+ overloaded as a friend function:

#include <iostream>

class Fraction
{
private:
    int m_numerator{};
    int m_denominator{1};

public:
    Fraction(int num, int denom = 1)
        : m_numerator{num}, m_denominator{denom} {}

    friend Fraction operator+(const Fraction& f, int value);

    void print() const
    {
        std::cout << m_numerator << '/' << m_denominator << '\n';
    }
};

// Not a member function
Fraction operator+(const Fraction& f, int value)
{
    return Fraction{f.m_numerator + value * f.m_denominator, f.m_denominator};
}

int main()
{
    const Fraction half{1, 2};
    const Fraction result{half + 2};
    result.print();

    return 0;
}

Converting a friend operator to a member operator is straightforward:

  1. Define the operator as a member (Fraction::operator+ instead of friend operator+)
  2. Remove the left parameter (it becomes the implicit *this object)
  3. Inside the function, remove references to the left parameter (e.g., f.m_numerator becomes m_numerator, which implicitly uses *this)

Here's the same operator as a member function:

#include <iostream>

class Fraction
{
private:
    int m_numerator{};
    int m_denominator{1};

public:
    Fraction(int num, int denom = 1)
        : m_numerator{num}, m_denominator{denom} {}

    Fraction operator+(int value) const;

    void print() const
    {
        std::cout << m_numerator << '/' << m_denominator << '\n';
    }
};

// Member function - the fraction parameter is now implicit *this
Fraction Fraction::operator+(int value) const
{
    return Fraction{m_numerator + value * m_denominator, m_denominator};
}

int main()
{
    const Fraction half{1, 2};
    const Fraction result{half + 2};
    result.print();

    return 0;
}

Usage doesn't change (half + 2), just the implementation. Our two-parameter friend function becomes a one-parameter member function, with the leftmost friend parameter (fraction) becoming the implicit *this in the member version.

Let's examine how half + 2 evaluates:

Friend function version: half + 2 becomes operator+(half, 2). Two explicit parameters.

Member function version: half + 2 becomes half.operator+(2). Only one explicit parameter, with half as the object prefix. However, the compiler implicitly converts the object prefix into a hidden leftmost parameter *this. So half.operator+(2) actually becomes operator+(&half, 2), nearly identical to the friend version.

Both produce the same result, just via slightly different mechanisms.

When You Must Use Member Functions

The assignment (=), subscript ([]), function call (()), and member selection (->) operators must be overloaded as member functions.

When You Cannot Use Member Functions

In the lesson on overloading I/O operators, we overloaded operator<< for our Point class using a friend function:

#include <iostream>

class Point
{
private:
    double m_x{};
    double m_y{};

public:
    Point(double x = 0.0, double y = 0.0)
        : m_x{x}, m_y{y}
    {}

    friend std::ostream& operator<<(std::ostream& out, const Point& pt);
};

std::ostream& operator<<(std::ostream& out, const Point& pt)
{
    out << "(" << pt.m_x << ", " << pt.m_y << ")";

    return out;
}

int main()
{
    Point origin{0.0, 0.0};

    std::cout << origin;

    return 0;
}

We cannot overload operator<< as a member function here. Why? Because the overloaded operator must be a member of the left operand's class. The left operand is std::ostream, which is part of the standard library. We cannot modify std::ostream to add our member function.

This requires operator<< to be a normal function (preferred) or friend.

Similarly, we can overload operator+(Fraction, int) as a member, but not operator+(int, Fraction), because int isn't a class we can add members to.

Generally, you cannot use member overloads when the left operand is either not a class (e.g., int) or is a class you cannot modify (e.g., std::ostream).

When to Use Normal, Friend, or Member Function Overloads

The language usually lets you choose between normal/friend or member function versions. However, one is typically better:

For binary operators that don't modify the left operand (e.g., operator+), prefer normal or friend functions. They work for all parameter types (even when the left operand isn't a class or is unmodifiable). They also provide "symmetry" - all operands are explicit parameters rather than one becoming *this.

For binary operators that modify the left operand (e.g., operator+=), prefer member functions. The leftmost operand is always a class type, and having the modified object be *this is natural. The rightmost operand being an explicit parameter clearly shows who gets modified versus evaluated.

Unary operators are usually overloaded as members since the member version has no parameters.

Rules of Thumb

  • If overloading assignment (=), subscript ([]), function call (()), or member selection (->), use a member function.
  • If overloading a unary operator, use a member function.
  • If overloading a binary operator that doesn't modify its left operand (e.g., operator+), use a normal function (preferred) or friend function.
  • If overloading a binary operator that modifies its left operand, but you cannot add members to the left operand's class (e.g., operator<< with std::ostream), use a normal function (preferred) or friend function.
  • If overloading a binary operator that modifies its left operand (e.g., operator+=), and you can modify the left operand's class, use a member function.

Summary

Member function operators: Overloaded operators implemented as member functions where the left operand becomes the implicit *this object and all other operands become function parameters.

Conversion from friend to member: Remove the left parameter (it becomes *this), define as a member function, and remove explicit references to the left parameter inside the function.

Required member overloads: Assignment (=), subscript ([]), function call (()), and member selection (->) operators must be overloaded as member functions.

Prohibited member overloads: Cannot use member functions when the left operand is not a class (like int) or is a class you cannot modify (like std::ostream). These require normal or friend functions.

When to use member vs friend/normal: Use member functions for unary operators and binary operators that modify the left operand (when you control that class). Use friend/normal functions for binary operators that don't modify operands, providing symmetric parameter treatment.

Member functions are natural for operators that modify the object they're called on, making the modified object (*this) explicit. They're required for certain operators and preferred for operators like += where the leftmost operand is clearly being modified.