Skip to content

Operator Overloading — Syntax, Stream Operators, Arithmetic, Type Conversion

DodaTech Updated 2026-06-28 10 min read

In this tutorial, you will learn about Operator Overloading. We cover key concepts, practical examples, and best practices to help you master this topic.

C++ operator overloading allows custom types to behave like built-in types by defining how operators such as +, -, <<, and >> work with class instances.

What You'll Learn

You will overload arithmetic operators (+, -, *, /), stream insertion and extraction operators (<<, >>), comparison operators (==, !=, <, >), the subscript operator ([]), the function call operator (()), and type conversion operators. You will also understand when to implement operators as member functions versus non-member friends.

Why It Matters

Operator overloading makes user-defined types feel like language primitives. A Matrix class with +, -, and * operators is far more readable than one that requires explicit add() and multiply() method calls. The standard library uses operator overloading extensively (e.g., std::cout << value, std::cin >> value, std::string + std::string).

Learning Path

graph LR
    A["18: Multiple Inheritance"] --> B["19: Operator Overloading"]
    B --> C["20: Copy & Move Semantics"]
    C --> D["21: Pointers"]
    style A fill:#4a90d9,stroke:#2c5f8a,color:#fff
    style B fill:#4a90d9,stroke:#2c5f8a,color:#fff
    style C fill:#4a90d9,stroke:#2c5f8a,color:#fff
    style D fill:#4a90d9,stroke:#2c5f8a,color:#fff

Basic Operator Overloading

#include <iostream>

class Vector2 {
private:
    double x_, y_;
    
public:
    Vector2(double x = 0, double y = 0) : x_(x), y_(y) {}
    
    // Arithmetic operators as member functions
    Vector2 operator+(const Vector2& other) const {
        return Vector2(x_ + other.x_, y_ + other.y_);
    }
    
    Vector2 operator-(const Vector2& other) const {
        return Vector2(x_ - other.x_, y_ - other.y_);
    }
    
    Vector2 operator*(double scalar) const {
        return Vector2(x_ * scalar, y_ * scalar);
    }
    
    Vector2& operator+=(const Vector2& other) {
        x_ += other.x_;
        y_ += other.y_;
        return *this;
    }
    
    // Unary minus
    Vector2 operator-() const {
        return Vector2(-x_, -y_);
    }
    
    // Access operator
    double operator[](size_t index) const {
        return index == 0 ? x_ : y_;
    }
    
    double& operator[](size_t index) {
        return index == 0 ? x_ : y_;
    }
    
    void print() const {
        std::cout << "(" << x_ << ", " << y_ << ")\n";
    }
};

int main() {
    Vector2 a(3, 4);
    Vector2 b(1, 2);
    
    Vector2 sum = a + b;
    Vector2 diff = a - b;
    Vector2 scaled = a * 2.0;
    Vector2 neg = -a;
    
    sum.print();
    diff.print();
    scaled.print();
    neg.print();
    
    a += b;
    a.print();
    
    std::cout << "a[0] = " << a[0] << "\n";
}

Member vs Non-Member Operators

#include <iostream>

class Value {
private:
    int v_;
    
public:
    Value(int v = 0) : v_(v) {}
    
    // Member: left operand is *this
    Value operator+(const Value& rhs) const {
        return Value(v_ + rhs.v_);
    }
    
    // Compound assignment: should be member
    Value& operator+=(const Value& rhs) {
        v_ += rhs.v_;
        return *this;
    }
    
    int value() const { return v_; }
    
    // Friend declaration for non-member operator
    friend Value operator*(int lhs, const Value& rhs);
};

// Non-member: allows int * Value (int is not a class)
Value operator*(int lhs, const Value& rhs) {
    return Value(lhs * rhs.v_);
}

// Non-member: allows Value * int (if not defined as member)
Value operator*(const Value& lhs, int rhs) {
    return Value(lhs.value() * rhs);
}

int main() {
    Value v1(5);
    Value v2(3);
    
    Value sum = v1 + v2;    // member
    Value mul1 = v1 * 2;    // non-member
    Value mul2 = 3 * v2;    // non-member (friend needed for private access)
    
    std::cout << sum.value() << "\n";
    std::cout << mul1.value() << "\n";
    std::cout << mul2.value() << "\n";
}

Rule: Implement compound assignment (+=, -=, etc.) as members. Implement symmetric operators (+, -, *) as non-members when the left operand might not be of the class type.

Stream Insertion and Extraction

#include <iostream>
#include <sstream>

class Point {
private:
    int x_, y_;
    
public:
    Point(int x = 0, int y = 0) : x_(x), y_(y) {}
    
    friend std::ostream& operator<<(std::ostream& os, const Point& p);
    friend std::istream& operator>>(std::istream& is, Point& p);
};

std::ostream& operator<<(std::ostream& os, const Point& p) {
    os << "(" << p.x_ << ", " << p.y_ << ")";
    return os;
}

std::istream& operator>>(std::istream& is, Point& p) {
    is >> p.x_ >> p.y_;
    return is;
}

int main() {
    Point p(3, 4);
    std::cout << "Point: " << p << "\n";
    
    std::cout << "Enter x y: ";
    Point input;
    if (std::cin >> input) {
        std::cout << "You entered: " << input << "\n";
    }
}

Stream operators must be non-member functions because the left operand (std::cout or std::cin) is a standard library type that cannot be modified.

Comparison Operators

#include <iostream>
#include <vector>
#include <algorithm>

class Fraction {
private:
    int num_, den_;
    
    void simplify() {
        int g = gcd(num_, den_);
        num_ /= g;
        den_ /= g;
        if (den_ < 0) { num_ = -num_; den_ = -den_; }
    }
    
    static int gcd(int a, int b) {
        return b == 0 ? (a < 0 ? -a : a) : gcd(b, a % b);
    }
    
public:
    Fraction(int n, int d) : num_(n), den_(d) {
        if (d == 0) throw std::runtime_error("denominator zero");
        simplify();
    }
    
    double value() const { return static_cast<double>(num_) / den_; }
    
    // Comparison operators
    bool operator==(const Fraction& other) const {
        return num_ == other.num_ && den_ == other.den_;
    }
    
    bool operator!=(const Fraction& other) const {
        return !(*this == other);
    }
    
    bool operator<(const Fraction& other) const {
        return num_ * other.den_ < other.num_ * den_;
    }
    
    bool operator>(const Fraction& other) const {
        return other < *this;
    }
    
    bool operator<=(const Fraction& other) const {
        return !(other < *this);
    }
    
    bool operator>=(const Fraction& other) const {
        return !(*this < other);
    }
    
    friend std::ostream& operator<<(std::ostream& os, const Fraction& f) {
        os << f.num_ << "/" << f.den_;
        return os;
    }
};

int main() {
    std::vector<Fraction> fractions = {
        Fraction(3, 4), Fraction(1, 2), Fraction(5, 6), Fraction(2, 3)
    };
    
    std::sort(fractions.begin(), fractions.end());
    
    for (const auto& f : fractions) {
        std::cout << f << " ";
    }
    std::cout << "\n";
}

Subscript Operator

#include <iostream>
#include <stdexcept>
#include <cassert>

class IntVector {
private:
    int* data_;
    size_t size_;
    
public:
    IntVector(size_t size) : data_(new int[size]()), size_(size) {}
    ~IntVector() { delete[] data_; }
    
    // Non-const access
    int& operator[](size_t index) {
        assert(index < size_);
        return data_[index];
    }
    
    // Const access
    const int& operator[](size_t index) const {
        assert(index < size_);
        return data_[index];
    }
    
    // Bounds-checked access
    int& at(size_t index) {
        if (index >= size_) throw std::out_of_range("index out of range");
        return data_[index];
    }
    
    size_t size() const { return size_; }
};

int main() {
    IntVector v(5);
    v[0] = 10;
    v[1] = 20;
    
    const IntVector& cv = v;
    std::cout << cv[0] << "\n";  // const version
}

Function Call Operator

#include <iostream>
#include <vector>
#include <algorithm>

class MultiplyBy {
private:
    double factor_;
    
public:
    MultiplyBy(double factor) : factor_(factor) {}
    
    double operator()(double x) const {
        return x * factor_;
    }
};

int main() {
    MultiplyBy doubler(2.0);
    MultiplyBy tripler(3.0);
    
    std::cout << doubler(5) << "\n";   // 10
    std::cout << tripler(5) << "\n";   // 15
    
    std::vector<double> data = {1.0, 2.0, 3.0, 4.0};
    std::transform(data.begin(), data.end(), data.begin(), MultiplyBy(10.0));
    
    for (double x : data) {
        std::cout << x << " ";
    }
    std::cout << "\n";
}

Objects that overload operator() are called functors or function objects. They can maintain state across calls.

Type Conversion Operators

#include <iostream>

class Rational {
private:
    int num_, den_;
    
public:
    Rational(int n, int d) : num_(n), den_(d) {}
    
    // Conversion to double
    operator double() const {
        return static_cast<double>(num_) / den_;
    }
    
    // Conversion to bool (non-zero check)
    explicit operator bool() const {
        return num_ != 0;
    }
    
    // Explicit conversion to int (truncates)
    explicit operator int() const {
        return num_ / den_;
    }
};

int main() {
    Rational r(3, 4);
    
    double d = r;     // implicit conversion to double
    std::cout << d << "\n";  // 0.75
    
    bool b = static_cast<bool>(r);  // explicit conversion
    std::cout << b << "\n";  // 1
    
    int i = static_cast<int>(r);  // explicit conversion
    std::cout << i << "\n";  // 0
}

Mark single-argument constructors and conversion operators as explicit to prevent unwanted implicit conversions.

Common Mistakes

Mistake 1: Overloading && and ||

Overloading logical && and || loses short-circuit evaluation. The overloaded versions evaluate both operands, which can break code that relies on short-circuit behavior.

Mistake 2: Not Handling const Correctly

Vector2 operator+(Vector2& other) { ... }  // cannot add const vectors

Mark operators that do not modify operands as const.

Mistake 3: Stream Operator Not Returning os or is

void operator<<(std::ostream& os, const Point& p) { ... }

Must return the stream reference for chaining.

Mistake 4: Inconsistent Comparison Operators

If you define ==, define !=. If you define <, define >, <=, >=. Use std::rel_ops or the spaceship operator (<=>) in C++20.

Mistake 5: Overloading operator,

The comma operator is almost never overloaded correctly. Avoid it.

Mistake 6: Forgetting Self-Assignment in Compound Operators

Vector2& operator+=(const Vector2& other) {
    // works fine for self-assignment (no special handling needed for addition)
    x_ += other.x_;
    return *this;
}

Self-assignment is safe for arithmetic compound operators but requires checks in copy assignment.

Practice Questions

  1. Why must stream insertion (<<) and extraction (>>) be non-member functions?
  2. When should you use a member function versus a non-member friend for operator overloading?
  3. Implement the spaceship operator (<=>) for the Fraction class.
  4. Write a RingBuffer class with operator[] and a functor-based for-each method.
  5. What is the purpose of explicit on conversion operators?

Challenge

Implement a BigInt class that can handle arbitrarily large integers. Overload +, -, *, /, %, ==, <, <<, >>, and []. Store digits in a std::vector<int>.

FAQ

Can I overload every operator?

No. You cannot overload ::, .*, ., ?:, sizeof, typeid, alignof, or noexcept. All other operators can be overloaded.

Can I change the precedence or associativity of an operator?

No. Operator overloading can only change the behavior for custom types, not the precedence, associativity, or arity.

What is the spaceship operator `<=>`?

C++20's three-way comparison operator returns an ordering type (strong_ordering, weak_ordering, partial_ordering). With = default, it generates all six comparison operators.

Should I overload `operator&`?

Almost never. Overloading operator& breaks getting the address of an object, which many templates and standard library features rely on.

What is the difference between prefix and postfix increment overloading?

Prefix: T& operator++(); Postfix: T operator++(int); (dummy int parameter distinguishes them).

Can I overload operators for built-in types?

No. At least one operand must be a user-defined type.

Mini Project

Build a Matrix2x2 class with full operator support:

#include <iostream>
#include <cmath>

class Matrix2x2 {
private:
    double data_[2][2];
    
public:
    Matrix2x2(double a = 0, double b = 0, double c = 0, double d = 0) {
        data_[0][0] = a; data_[0][1] = b;
        data_[1][0] = c; data_[1][1] = d;
    }
    
    // Access
    double* operator[](size_t row) { return data_[row]; }
    const double* operator[](size_t row) const { return data_[row]; }
    
    // Arithmetic
    Matrix2x2 operator+(const Matrix2x2& m) const {
        return Matrix2x2(
            data_[0][0] + m.data_[0][0], data_[0][1] + m.data_[0][1],
            data_[1][0] + m.data_[1][0], data_[1][1] + m.data_[1][1]
        );
    }
    
    Matrix2x2 operator*(const Matrix2x2& m) const {
        Matrix2x2 result;
        for (int r = 0; r < 2; ++r)
            for (int c = 0; c < 2; ++c)
                for (int k = 0; k < 2; ++k)
                    result[r][c] += data_[r][k] * m[k][c];
        return result;
    }
    
    double determinant() const {
        return data_[0][0] * data_[1][1] - data_[0][1] * data_[1][0];
    }
    
    friend std::ostream& operator<<(std::ostream& os, const Matrix2x2& m) {
        os << "[" << m.data_[0][0] << " " << m.data_[0][1] << "]\n";
        os << "[" << m.data_[1][0] << " " << m.data_[1][1] << "]";
        return os;
    }
};

int main() {
    Matrix2x2 a(1, 2, 3, 4);
    Matrix2x2 b(5, 6, 7, 8);
    
    std::cout << "a:\n" << a << "\n\n";
    std::cout << "a + b:\n" << (a + b) << "\n\n";
    std::cout << "a * b:\n" << (a * b) << "\n\n";
    std::cout << "det(a) = " << a.determinant() << "\n";
}

What's Next

Operator overloading makes custom types expressive. The next lesson covers copy and move semantics in depth: the rule of three and rule of five, move constructors, move assignment, and when the compiler generates these special member functions.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro