Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Defining Custom Behaviors for C++ Operators

Tech May 8 4

The C++ language allows operators to be redefined sothat they work with user‑defined types in a natural way. This mechanism is called operator overloading and is essentially syntactic sugar for function calls.

Adding Two Custom Objects

When you need to combine two objects of a class, you can provide a dedicated member function, but overloading + makes the code much cleaner.

Member Function Approach

#include <iostream>
using namespace std;

class Vector2D {
public:
    int x;
    int y;

    Vector2D(int xVal, int yVal) : x(xVal), y(yVal) {}

    Vector2D operator+(const Vector2D &other) const {
        return Vector2D(x + other.x, y + other.y);
    }
};

int main() {
    Vector2D v1(3, 7);
    Vector2D v2(8, 2);
    Vector2D v3 = v1 + v2;

    cout << "Result: (" << v3.x << ", " << v3.y << ")" << endl;
    return 0;
}

Global Function Approach

#include <iostream>
using namespace std;

class Vector2D {
public:
    int x;
    int y;

    Vector2D(int xVal, int yVal) : x(xVal), y(yVal) {}
};

Vector2D operator+(const Vector2D &lhs, const Vector2D &rhs) {
    return Vector2D(lhs.x + rhs.x, lhs.y + rhs.y);
}

int main() {
    Vector2D v1(5, 9);
    Vector2D v2(2, 3);
    Vector2D v3 = v1 + v2;

    cout << "Sum: (" << v3.x << ", " << v3.y << ")" << endl;
    return 0;
}

Overloaded operators can themselves be overloaded — different parameter lists allow the same operator symbol to work with multiple type combinations.

#include <iostream>
using namespace std;

class Vector2D {
public:
    int x, y;
    Vector2D(int xVal, int yVal) : x(xVal), y(yVal) {}
};

Vector2D operator+(const Vector2D &v, int scalar) {
    return Vector2D(v.x + scalar, v.y + scalar);
}

int operator+(const Vector2D &v, const int &magic) {
    return v.x + v.y + magic;
}

int main() {
    Vector2D a(1, 4);
    Vector2D b(6, 2);

    Vector2D c = a + b;         // uses Vector2D + Vector2D
    Vector2D d = a + 10;        // uses Vector2D + int -> Vector2D
    int total = a + 100;        // uses Vector2D + const int -> int

    cout << "d: (" << d.x << ", " << d.y << ")" << endl;
    cout << "Total: " << total << endl;
    return 0;
}

Key points:

  • Only user‑defined types can participate in operator overloading; built‑in operand meanings cannot be altered.
  • Keep the semantics intuitive — avoid surprises that reduce readability.

Streaming Output with <<

To print objects directly with cout, overload the left‑shift operator. Since cout is an ostream object, a global overload with a friend declaration works best.

Incorrect Member Overload

Defining << as a member forces the object to appear on the left, producing awkward syntax.

#include <iostream>
using namespace std;

class Point {
    int px, py;
public:
    Point(int xx, int yy) : px(xx), py(yy) {}

    ostream& operator<<(ostream &os) {
        os << "(" << px << ", " << py << ")";
        return os;
    }
};

int main() {
    Point p1(12, 5);
    Point p2(7, 30);
    (p2 << (p1 << cout)) << endl;
    return 0;
}

Correct Global Overload with Friend

#include <iostream>
using namespace std;

class Point {
    friend ostream& operator<<(ostream &os, const Point &pt);
private:
    int px, py;
public:
    Point(int xx, int yy) : px(xx), py(yy) {}
};

ostream& operator<<(ostream &os, const Point &pt) {
    os << "[" << pt.px << ", " << pt.py << "]";
    return os;
}

int main() {
    Point a(4, 9);
    Point b(10, 2);
    cout << a << " | " << b << endl;
    return 0;
}

Returning a reference to ostream enables chaining. If data members are private, either add public getters or declare the global operator as a friend.

Increment and Decrement Operators

Overloading ++ and -- requires handling both prefix and postfix forms. The postfix version accepts a dummy int parameter to differantiate it from the prefix form.

#include <iostream>
using namespace std;

class Counter {
    friend ostream& operator<<(ostream &os, const Counter &c);
private:
    int value;
public:
    Counter() : value(0) {}

    // Prefix ++: increments and returns a reference for chaining
    Counter& operator++() {
        ++value;
        return *this;
    }

    // Postfix ++: saves state, increments, returns constant copy
    const Counter operator++(int) {
        Counter previous = *this;
        value++;
        return previous;
    }
};

ostream& operator<<(ostream &os, const Counter &c) {
    os << c.value;
    return os;
}

int main() {
    Counter c1, c2;
    cout << ++c1 << endl;        // 1
    cout << ++++c1 << endl;      // 3 (chained prefix)

    cout << c2++ << endl;        // 0
    cout << c2 << endl;          // 1

    // ++(c2++);                 // compilation error (postfix returns const)
    return 0;
}

For global overloads, the object must be passed by reference and the operators should often be friends.

#include <iostream>
using namespace std;

class Meter {
    friend ostream& operator<<(ostream &os, const Meter &m);
    friend Meter& operator--(Meter &m);
    friend const Meter operator--(Meter &m, int);
private:
    int length;
public:
    Meter(int l) : length(l) {}
};

ostream& operator<<(ostream &os, const Meter &m) {
    os << m.length << "m";
    return os;
}

Meter& operator--(Meter &m) {
    --m.length;
    return m;
}

const Meter operator--(Meter &m, int) {
    Meter old = m;
    m.length--;
    return old;
}

int main() {
    Meter m1(5), m2(10);
    cout << --m1 << endl;
    cout << m2-- << endl;
    cout << m2 << endl;
    return 0;
}

Prefix operators should return a non‑const reference to allow chain expressions. Postfix versions should return a const object to prevent misuse such as obj++++.

Assignment Operator and Deep Copy

The compiler generates a default operator= that performs member‑wise shallow copy. When a class manages heap resources, a custom version is essential.

#include <iostream>
using namespace std;

class Buffer {
    int *data;
public:
    Buffer(int val) {
        data = new int(val);
    }

    ~Buffer() {
        delete data;
        data = nullptr;
    }

    // Deep‑copy assignment
    Buffer& operator=(const Buffer &src) {
        if (this == &src) return *this;   // self‑assignment guard

        delete data;
        data = new int(*src.data);
        return *this;
    }

    int value() const { return *data; }
};

int main() {
    Buffer b1(42);
    Buffer b2(7);
    b2 = b1;
    cout << b2.value() << endl;   // 42
    return 0;
}

Choosing between returning a reference or a value matters:

  • Returning Buffer& avoids extra copy construction calls.
  • Returning Buffer by value triggers a copy constructor, which must itself perform a deep copy to prevent double deletion.

Relational Operators

Relational operators (==, !=, <, etc.) let you compare objects naturally.

#include <iostream>
#include <string>
using namespace std;

class Student {
    string id;
    int grade;
public:
    Student(string id, int grade) : id(id), grade(grade) {}

    bool operator==(const Student &other) const {
        return id == other.id && grade == other.grade;
    }

    friend bool operator!=(const Student &a, const Student &b) {
        return !(a == b);
    }
};

int main() {
    Student alice("A01", 85);
    Student bob("A02", 85);
    cout << boolalpha << (alice == bob) << endl;   // false
    cout << (alice != bob) << endl;                // true
    return 0;
}

Function Call Operator (Functors)

Overloading operator() turns objects into callable entities known as functors.

#include <iostream>
#include <string>
using namespace std;

class Logger {
public:
    void operator()(const string &msg) {
        cout << "[LOG] " << msg << endl;
    }

    int operator()(int a, int b) {
        return a * b;
    }
};

int main() {
    Logger log;
    log("System started");

    int product = log(6, 7);
    cout << product << endl;   // 42

    // Using an anonymous object
    cout << Logger()(2, 3) << endl;
    return 0;
}

Note that operator() must be defined as a non‑static member function; it cannot appear at global scope.

Tags: C++

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.