Operator Overloading & Rule of Three/Five

অপারেটর ওভারলোডিং ও Rule of Three/Five

Read: ~35 min 14 practice problems

1. Make Your Type Feel Built-in

Operator overloading lets you write a + b for your type. Use it for value-like types (vectors, fractions, complex numbers).

overload.cpp
#include <iostream>

struct Vec2 {
    double x, y;

    Vec2 operator+(const Vec2& o) const { return {x + o.x, y + o.y}; }
    Vec2 operator-(const Vec2& o) const { return {x - o.x, y - o.y}; }
    Vec2 operator*(double s)     const { return {x * s, y * s}; }
    bool operator==(const Vec2&) const = default; // C++20
};

int main() {
    Vec2 a{1, 2}, b{3, 4};
    Vec2 c = a + b;
    std::cout << c.x << ", " << c.y << "\n";
}

2. operator<< for Streams

Stream output overloads as a free function (must take ostream by reference):

stream.cpp
#include <iostream>

struct Vec2 { double x, y; };

std::ostream& operator<<(std::ostream& os, const Vec2& v) {
    return os << "(" << v.x << ", " << v.y << ")";
}

int main() {
    Vec2 v{3, 4};
    std::cout << v << "\n";
}

3. The Rule of Three / Five / Zero

RuleWhat it says
Rule of ThreeIf you write any of {dtor, copy ctor, copy assign}, write all three.
Rule of FiveAdds move ctor and move assign — write all five for move-aware classes.
Rule of ZeroDon't write any of them. Use members that manage themselves (smart pointers, vectors, strings).
Prefer Rule of Zero Compose your class from members that manage their own resources. The compiler-generated specials work correctly.

4. The Big Five Example

five.cpp
class Buffer {
    char* data_;
    size_t n_;
public:
    Buffer(size_t n) : data_(new char[n]), n_(n) {}
    ~Buffer() { delete[] data_; }

    // Copy
    Buffer(const Buffer& o) : data_(new char[o.n_]), n_(o.n_) {
        std::copy(o.data_, o.data_ + n_, data_);
    }
    Buffer& operator=(const Buffer& o) {
        if (this != &o) {
            delete[] data_;
            n_ = o.n_;
            data_ = new char[n_];
            std::copy(o.data_, o.data_ + n_, data_);
        }
        return *this;
    }

    // Move (noexcept!)
    Buffer(Buffer&& o) noexcept : data_(o.data_), n_(o.n_) {
        o.data_ = nullptr; o.n_ = 0;
    }
    Buffer& operator=(Buffer&& o) noexcept {
        if (this != &o) {
            delete[] data_;
            data_ = o.data_; n_ = o.n_;
            o.data_ = nullptr; o.n_ = 0;
        }
        return *this;
    }
};

(In real life: just use std::vector<char> and write nothing!)

5. =default and =delete

  • = default: ask compiler to generate the standard implementation.
  • = delete: forbid that operation entirely.

Make a class non-copyable: X(const X&) = delete; X& operator=(const X&) = delete;

6. Practice Problems

  1. Overload + for two Fractions.
    ✨ Show Answer
    Fraction operator+(const Fraction& o) const {
        return {num_*o.denom_ + o.num_*denom_, denom_*o.denom_};
    }
  2. When should you make a class non-copyable?
    ✨ Show Answer

    When the resource it owns can't be sensibly copied — file handles, mutexes, sockets, unique_ptr-like ownership.

  3. What's the Rule of Zero?
    ✨ Show Answer

    Design classes such that you don't need to write any of the special members. Compose with self-managing types. Compiler defaults work correctly.

  4. Define operator<< for printing a Date.
    ✨ Show Answer
    std::ostream& operator<<(std::ostream& os, const Date& d) {
        return os << d.year << "-" << d.month << "-" << d.day;
    }
  5. Why is move assignment marked noexcept?
    ✨ Show Answer

    STL containers (e.g. vector) only use move during reallocation if move is noexcept. Otherwise they copy (slower) for strong exception safety.

  6. Why default the comparison with = default?
    ✨ Show Answer

    C++20 generates element-wise comparison automatically — saves boilerplate, reduces bugs.

  7. What does X(const X&) = delete mean?
    ✨ Show Answer

    The class is non-copyable. Any attempt to copy fails to compile.

  8. Overload prefix and postfix ++.
    ✨ Show Answer
    X& operator++()    { ++n_; return *this; }     // prefix
    X  operator++(int) { X t = *this; ++n_; return t; } // postfix (dummy int)
  9. Why must operator= handle self-assignment?
    ✨ Show Answer

    x = x can happen. If you naively delete then copy, you delete what you're trying to copy. Check this != &o or use copy-and-swap.

  10. Show copy-and-swap idiom.
    ✨ Show Answer
    Buffer& operator=(Buffer o) // pass by value: caller copies/moves
    {
        swap(*this, o);
        return *this;
    } // o destroyed automatically with old data

    Self-assignment-safe and exception-safe.

  11. When does the compiler NOT auto-generate the move ctor?
    ✨ Show Answer

    If you've declared any of: copy ctor, copy assign, move assign, destructor. Then it disables the default move. Use = default to bring it back.

  12. What does std::move(x) do?
    ✨ Show Answer

    Casts x to an rvalue reference, telling the compiler "you may steal from x". Doesn't actually move anything; the move ctor / assign operator does the work.

  13. Overload operator[].
    ✨ Show Answer
    int& operator[](size_t i)       { return data_[i]; }
    const int& operator[](size_t i) const { return data_[i]; }
  14. Define a class member function as deleted to forbid an overload.
    ✨ Show Answer
    class X {
    public:
        void f(int);
        void f(double) = delete; // forbid double conversion
    };

Summary

Operator overloading makes user types feel native. Rule of Five reminds you to handle all five special functions when one is needed. Rule of Zero says: design so you don't need to write any. Mark moves noexcept. Use = default / = delete instead of hand-writing trivial members.

Next Module → Function Templates & Generic Programming.