8  Operator Overloading and Friend Functions

9 Operator Overloading and Friend Functions

Week 3, Session 2. CSS 342.

9.1 Warmup

Palindrome Number (LeetCode #9) — revisit. This time write operator== for a custom IntPair class and use it to compare the front and back digits.

9.2 Learning objectives

  1. Overload comparison operators (==, !=, <, >, <=, >=) to enable objects to be sorted and compared.
  2. Implement operator<< as a friend function to integrate a class with std::cout.
  3. Implement operator>> to enable std::cin input into a class.
  4. Explain the difference between member and non-member operator overloading and choose correctly for << / >>.
  5. Simplify boolean return patterns (replace if (x) return true; else return false; with return x).

9.3 Why overload operators?

So your class participates in the language. sort, set, map, cout — they all use operators. If your class defines operator<, you can put it in a set. If your class defines operator<<, you can print it with cout. The point is uniform syntax: your type behaves like a built-in.

9.4 Comparison operators

A Box class with width and height. We want == (same dimensions) and < (smaller area).

class Box {
 public:
  Box(int w, int h) : width_(w), height_(h) {}
  int area() const { return width_ * height_; }

  bool operator==(const Box& other) const {
    return width_ == other.width_ && height_ == other.height_;
  }
  bool operator<(const Box& other) const {
    return area() < other.area();
  }

 private:
  int width_;
  int height_;
};

Now:

Box a(2, 3);
Box b(3, 2);
Box c(5, 5);

cout << (a == b) << endl;   // 0 (different dims, same area)
cout << (a < c) << endl;    // 1

set<Box> boxes;             // works! set needs operator<
boxes.insert(a);
boxes.insert(c);

Once you have == and <, the others (!=, >, <=, >=) follow logically. In C++20 you can use auto operator<=>(const Box&) const = default; and get all six for free. We are sticking with C++17 in this book, so write them explicitly when you need them.

9.5 operator<< — printing

This is the most useful one. It lets you cout << myBox.

// In Box.h, declare as friend
class Box {
  friend ostream& operator<<(ostream& os, const Box& b);
  // ... rest of class
};

// In Box.cpp, define as non-member
ostream& operator<<(ostream& os, const Box& b) {
  os << "<Box " << b.width_ << "x" << b.height_ << ">";
  return os;
}

Why non-member? Because the left operand is ostream, not Box. We cannot add member functions to ostream — it is not our class. So operator<< must be a free function. The friend declaration just gives it access to private fields.

Why return ostream&? Because of chaining:

cout << "Box a: " << a << " has area " << a.area() << endl;

Each << returns its ostream so the next << can use it.

9.6 operator>> — reading

Symmetric to << but for input:

istream& operator>>(istream& is, Box& b) {
  int w, h;
  is >> w >> h;
  b = Box(w, h);   // or set fields directly if friend
  return is;
}

Used as:

Box b(0, 0);
cin >> b;          // reads two ints into the box

9.7 Member vs. non-member — the rule

Operator Member or non-member? Why
+, -, *, / Either, often member Symmetric — left operand is your class
+=, -=, *= Always member Modifies left operand
==, <, > Either, often member Comparison
<<, >> Non-member (friend) Left operand is stream
= (assignment) Always member Implicit, must be member
[] Always member Indexing on self

When in doubt: member if the left operand is your class, non-member otherwise.

9.8 Compound assignment

class Fraction {
 public:
  Fraction(int n, int d) : num_(n), den_(d) {}

  Fraction& operator+=(const Fraction& other) {
    num_ = num_ * other.den_ + other.num_ * den_;
    den_ = den_ * other.den_;
    return *this;            // return ref to self for chaining
  }

  // operator+ can be implemented in terms of +=
  Fraction operator+(const Fraction& other) const {
    Fraction result = *this;
    result += other;
    return result;
  }

 private:
  int num_, den_;
};

That return *this; is critical. *this is the current object (dereferenced from this, which is a pointer). Returning a reference allows chains like a += b += c.

9.9 Style: simplify boolean returns

A pattern I see in P1 feedback that needs to die:

// Don't write this:
bool isEmpty() const {
  if (size_ == 0) {
    return true;
  } else {
    return false;
  }
}

// Write this:
bool isEmpty() const {
  return size_ == 0;
}

If the thing you want to return is already a bool, return it. Wrapping it in if/else true/false is a tell that you have not internalized that == produces a boolean.

The same applies to:

// Bad
return x != y ? true : false;

// Good
return x != y;

9.10 Try it

Add operator< to Fraction so that fractions can be sorted in ascending order. Hint: avoid floating point. Cross-multiply: a/b < c/d is equivalent to a*d < c*b (when b and d are positive).

bool Fraction::operator<(const Fraction& other) const {
  return num_ * other.den_ < other.num_ * den_;
}

Test it by inserting a few fractions into set<Fraction> and iterating.

9.11 Self-check

1. Why is operator<< typically a non-member friend function?
c. Member functions are tied to their class; you cannot add a member to ostream. A non-member free function with friend access is the idiom.
2. Refactor if (n > 0) { return true; } else { return false; }:
a. The comparison already produces a bool; wrapping it in if/else is noise.

9.12 Challenges

  1. Add operator<< and operator< to your Fraction class. Sort a vector<Fraction> and print.
  2. Design HashMap (#706) — class design with operators for clear use.
  3. Build a Money class with operator+, operator-, operator* (by an int), and operator<<.

9.13 Where this fits

You can now make your class behave like a value type. Next: what happens when those values are copied.

You are here Coming up
Operator overloading, friends, stream operators, member vs. non-member Chapter 7: copy constructors and the Rule of Three