Overloading the Arithmetic Operators Using Friend Functions

The arithmetic operators (+, -, *, /) are among the most commonly used operators in C++. These are all binary operators (taking two operands - one on each side). They can all be overloaded following the same pattern.

There are three approaches to overloading operators: member functions, friend functions, and normal functions. This lesson covers the friend function approach (the most intuitive for binary operators). The next lesson discusses normal functions, and a later lesson covers member functions. We'll also summarize when to use each approach.

Overloading Operators Using Friend Functions

Consider this class representing weight:

class Weight
{
private:
    double m_kilograms{};

public:
    explicit Weight(double kg) : m_kilograms{kg} {}
    double getKilograms() const { return m_kilograms; }
};

Here's how to overload operator+ to add two Weight objects:

#include <iostream>

class Weight
{
private:
    double m_kilograms{};

public:
    explicit Weight(double kg) : m_kilograms{kg} {}

    friend Weight operator+(const Weight& w1, const Weight& w2);

    double getKilograms() const { return m_kilograms; }
};

// Note: not a member function!
Weight operator+(const Weight& w1, const Weight& w2)
{
    // Access m_kilograms directly (friend function)
    return Weight{w1.m_kilograms + w2.m_kilograms};
}

int main()
{
    Weight apple{0.25};
    Weight orange{0.3};
    Weight total{apple + orange};
    std::cout << "Total weight: " << total.getKilograms() << " kg\n";

    return 0;
}

Output:

Total weight: 0.55 kg

Overloading + requires declaring a function named operator+ with two parameters (the operand types), choosing an appropriate return type, and implementing the logic.

For our Weight class: we're adding two Weight objects, so we take two Weight parameters. We want to return a Weight result, so that's our return type.

Implementation: adding two Weight objects means adding their m_kilograms members. Since our overloaded operator+ is a friend, we can access m_kilograms directly. Since m_kilograms is a double and C++ knows how to add doubles, we simply use the built-in + operator.

Overloading subtraction (-) follows the same pattern:

#include <iostream>

class Weight
{
private:
    double m_kilograms{};

public:
    explicit Weight(double kg) : m_kilograms{kg} {}

    friend Weight operator+(const Weight& w1, const Weight& w2);
    friend Weight operator-(const Weight& w1, const Weight& w2);

    double getKilograms() const { return m_kilograms; }
};

Weight operator+(const Weight& w1, const Weight& w2)
{
    return Weight{w1.m_kilograms + w2.m_kilograms};
}

Weight operator-(const Weight& w1, const Weight& w2)
{
    return Weight{w1.m_kilograms - w2.m_kilograms};
}

int main()
{
    Weight package{5.0};
    Weight contents{3.2};
    Weight packaging{package - contents};
    std::cout << "Packaging weight: " << packaging.getKilograms() << " kg\n";

    return 0;
}

Overloading multiplication (*) and division (/) works the same way by defining operator* and operator/ respectively.

Friend Functions Can Be Defined Inside the Class

While friend functions aren't class members, they can be defined inside the class:

#include <iostream>

class Weight
{
private:
    double m_kilograms{};

public:
    explicit Weight(double kg) : m_kilograms{kg} {}

    // Friend function defined inside the class (still not a member!)
    friend Weight operator+(const Weight& w1, const Weight& w2)
    {
        return Weight{w1.m_kilograms + w2.m_kilograms};
    }

    double getKilograms() const { return m_kilograms; }
};

int main()
{
    Weight item1{1.5};
    Weight item2{2.3};
    Weight combined{item1 + item2};
    std::cout << "Combined weight: " << combined.getKilograms() << " kg\n";

    return 0;
}

This is convenient for trivial implementations.

Overloading Operators for Operands of Different Types

Often you'll want overloaded operators to work with mixed types. For example, if we have Weight{2.0}, we might want to multiply by the integer 3 to get Weight{6.0}.

When C++ evaluates x * y, x becomes the first parameter and y becomes the second. When both have the same type, order doesn't matter - x * y and y * x call the same function. However, with different types, x * y calls a different function than y * x.

For example, Weight{2.0} * 3 calls operator*(Weight, int), while 3 * Weight{2.0} calls operator*(int, Weight). We need to write both:

#include <iostream>

class Weight
{
private:
    double m_kilograms{};

public:
    explicit Weight(double kg) : m_kilograms{kg} {}

    friend Weight operator*(const Weight& w, double factor);
    friend Weight operator*(double factor, const Weight& w);

    double getKilograms() const { return m_kilograms; }
};

Weight operator*(const Weight& w, double factor)
{
    return Weight{w.m_kilograms * factor};
}

Weight operator*(double factor, const Weight& w)
{
    return Weight{w.m_kilograms * factor};
}

int main()
{
    Weight single{0.5};
    Weight batch1{single * 6};
    Weight batch2{4 * single};

    std::cout << "Batch 1: " << batch1.getKilograms() << " kg\n";
    std::cout << "Batch 2: " << batch2.getKilograms() << " kg\n";

    return 0;
}

Both functions have identical implementations since they perform the same operation with parameters in different orders.

Another Example

Let's create a class representing a 2D rectangle that can be scaled and combined:

#include <iostream>

class Rectangle
{
private:
    double m_width{};
    double m_height{};

public:
    Rectangle(double width, double height)
        : m_width{width}, m_height{height}
    {}

    double getWidth() const { return m_width; }
    double getHeight() const { return m_height; }
    double getArea() const { return m_width * m_height; }

    friend Rectangle operator+(const Rectangle& r1, const Rectangle& r2);
    friend Rectangle operator*(const Rectangle& r, double scale);
    friend Rectangle operator*(double scale, const Rectangle& r);
};

Rectangle operator+(const Rectangle& r1, const Rectangle& r2)
{
    // Combining rectangles: use max dimensions
    double newWidth{r1.m_width > r2.m_width ? r1.m_width : r2.m_width};
    double newHeight{r1.m_height > r2.m_height ? r1.m_height : r2.m_height};

    return Rectangle{newWidth, newHeight};
}

Rectangle operator*(const Rectangle& r, double scale)
{
    return Rectangle{r.m_width * scale, r.m_height * scale};
}

Rectangle operator*(double scale, const Rectangle& r)
{
    return r * scale; // Reuse the other overload
}

int main()
{
    Rectangle box1{3.0, 2.0};
    Rectangle box2{2.0, 4.0};
    Rectangle box3{1.0, 1.0};

    Rectangle combined{(box1 + box2 + box3) * 2.0};

    std::cout << "Combined: " << combined.getWidth() << " x "
              << combined.getHeight() << " = " << combined.getArea() << " sq units\n";

    return 0;
}

Output:

Combined: 6 x 8 = 48 sq units

The Rectangle class tracks width and height. We've overloaded + to combine rectangles (taking maximum dimensions) and * for scaling.

The expression (box1 + box2 + box3) * 2.0 evaluates left to right:

  • box1 + box2 produces Rectangle{3.0, 4.0}
  • Rectangle{3.0, 4.0} + box3 produces Rectangle{3.0, 4.0}
  • Rectangle{3.0, 4.0} * 2.0 produces Rectangle{6.0, 8.0}

Each operation returns a Rectangle object used as an operand for the next operation.

Implementing Operators Using Other Operators

In the example above, operator*(double, Rectangle) calls operator*(Rectangle, double), reducing duplication and simplifying maintenance. When one operator can be implemented by calling another producing the same result, do so.

Quiz Time

Question 1a: Write a class named Fraction with integer numerator and denominator members. Write a print() function.

The following should compile:

#include <iostream>

int main()
{
    Fraction f1{1, 2};
    f1.print();

    Fraction f2{3, 4};
    f2.print();

    return 0;
}

Expected output:

1/2
3/4
Show Solution
#include <iostream>

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

public:
    Fraction(int numerator, int denominator = 1)
        : m_numerator{numerator}, m_denominator{denominator}
    {}

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

int main()
{
    Fraction f1{1, 2};
    f1.print();

    Fraction f2{3, 4};
    f2.print();

    return 0;
}

Question 1b: Add overloaded * operators for Fraction * Fraction and Fraction * int.

The following should compile:

#include <iostream>

int main()
{
    Fraction f1{1, 2};
    f1.print();

    Fraction f2{3, 4};
    f2.print();

    Fraction f3{f1 * f2};
    f3.print();

    Fraction f4{f1 * 2};
    f4.print();

    Fraction f5{3 * f2};
    f5.print();

    Fraction f6{Fraction{1, 2} * Fraction{2, 3} * Fraction{3, 4}};
    f6.print();

    return 0;
}

Expected output:

1/2
3/4
3/8
2/2
9/4
6/24
Show Solution
#include <iostream>

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

public:
    Fraction(int numerator, int denominator = 1)
        : m_numerator{numerator}, m_denominator{denominator}
    {}

    friend Fraction operator*(const Fraction& f1, const Fraction& f2);
    friend Fraction operator*(const Fraction& f, int value);
    friend Fraction operator*(int value, const Fraction& f);

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

Fraction operator*(const Fraction& f1, const Fraction& f2)
{
    return Fraction{f1.m_numerator * f2.m_numerator,
                    f1.m_denominator * f2.m_denominator};
}

Fraction operator*(const Fraction& f, int value)
{
    return Fraction{f.m_numerator * value, f.m_denominator};
}

Fraction operator*(int value, const Fraction& f)
{
    return f * value;
}

int main()
{
    Fraction f1{1, 2};
    f1.print();

    Fraction f2{3, 4};
    f2.print();

    Fraction f3{f1 * f2};
    f3.print();

    Fraction f4{f1 * 2};
    f4.print();

    Fraction f5{3 * f2};
    f5.print();

    Fraction f6{Fraction{1, 2} * Fraction{2, 3} * Fraction{3, 4}};
    f6.print();

    return 0;
}

Question 1c: Why does the program work if we make the constructor non-explicit and remove the integer overloads?

Show Solution

With a non-explicit constructor, when we multiply a Fraction by an integer (or vice versa), the integer is implicitly converted to a Fraction using the constructor. This temporary Fraction is then used with operator*(Fraction, Fraction).

This is slightly less efficient than having dedicated integer overloads.

Question 1d: If we make operator*(Fraction, Fraction) take non-const references, this line fails:

Fraction f6{Fraction{1, 2} * Fraction{2, 3} * Fraction{3, 4}};

Why?

Show Solution

We're multiplying temporary Fraction objects. Non-const references cannot bind to temporaries.

Summary

Friend function operators: Overloaded operators implemented as friend functions have access to private members but aren't class members themselves.

Arithmetic operator pattern: Binary arithmetic operators like +, -, *, / follow the same overloading pattern - take two const references as parameters, return by value, and implement using the underlying member operations.

Inline friend definitions: Friend functions can be defined inside the class definition for convenience with trivial implementations, though they remain non-member functions.

Mixed-type operations: When overloading operators for different operand types (e.g., Weight * double), both orders may need separate overloads since x * y and y * x call different functions when types differ.

Implementing operators using others: When one operator can be implemented by calling another (e.g., operator*(double, Rectangle) calling operator*(Rectangle, double)), do so to reduce code duplication and simplify maintenance.

Operator chaining: Returning by value allows chaining multiple operations, as each operation produces a new object that becomes an operand for the next operation.

Friend functions are ideal for binary operators that don't modify operands, providing symmetric access to both parameters and working with all parameter types (including when the left operand isn't a class or is unmodifiable). The next lessons cover normal functions and member functions as alternatives.