Close Menu
GeekBlog

    Subscribe to Updates

    Get the latest creative news from FooBar about art, design and business.

    What's Hot

    Meta Wants an AI Agent Managing Your Life. Wall Street Isn’t So Sure

    August 2, 2026

    MakuluLinux’s New AI-OS Wants to Run Your Whole Desktop, Not Just Answer Questions

    August 1, 2026

    AI Tokens Got 98% Cheaper. Corporate AI Bills Are Exploding Anyway

    July 31, 2026
    Facebook X (Twitter) Instagram Threads
    GeekBlog
    • Home
    • Mobile
    • Tech News
    • Blog
    • How-To Guides
    • AI & Software
    Facebook
    GeekBlog
    Home»Blog»How to Overload an Operator in C++ (Complete Guide)
    Blog

    How to Overload an Operator in C++ (Complete Guide)

    Ethan CaldwellBy Ethan CaldwellJuly 30, 2026Updated:July 30, 202622 Mins Read
    Share Facebook Twitter Pinterest LinkedIn Tumblr Email Copy Link
    C++ class code on a laptop screen showing how to overload an operator in C++
    Share
    Facebook Twitter LinkedIn Pinterest Email Copy Link

    C++ has operator overloading, not operator overriding. To overload an operator in C++, define a function named operator@ where @ is the symbol, either as a member of your class or as a free function taking your type as a parameter. “Overriding” is what you do to a virtual member function in a derived class, and it is a completely different mechanism.

    Quick answer: Write ReturnType operator+(const T& rhs) const inside the class, or ReturnType operator+(const T& lhs, const T& rhs) outside it. Put compound assignment (+=, -=) and [], (), =, -> in the class; put symmetric binary operators and << / >> outside it. Since C++20, auto operator<=>(const T&) const = default; generates all six comparisons in one line.

    Every example below was compiled with g++ -std=c++20 -Wall -Wextra -pedantic on GCC 13.3 and run, and the outputs shown are real. The one C++23 example is compiled with -std=c++23 and marked as such.

    Overloading vs overriding: the terminology, settled

    These get mixed up constantly, and plenty of people search for “override an operator in C++” when they want overloading. Here is the distinction in one program.

    #include <iostream>
    
    // OVERRIDING: replacing a virtual function's behavior in a derived class.
    struct Shape {
        virtual ~Shape() = default;
        virtual double area() const { return 0.0; }
    };
    
    struct Circle : Shape {
        double r;
        explicit Circle(double radius) : r(radius) {}
        double area() const override { return 3.14159265358979 * r * r; }
    };
    
    // OVERLOADING: two functions with the same name and different parameters.
    int  scale(int  x) { return x * 2; }
    double scale(double x) { return x * 2.5; }
    
    int main() {
        const Shape& s = Circle{2.0};
        std::cout << "overridden area(): " << s.area() << '\n';
        std::cout << "scale(3)   -> " << scale(3) << '\n';
        std::cout << "scale(3.0) -> " << scale(3.0) << '\n';
    }
    OverloadingOverriding
    ResolvedAt compile time, by parameter typesAt run time, through the vtable
    Needs inheritanceNoYes, plus a virtual base function
    Applies to operatorsYes, this is the whole topicOnly if the operator is itself virtual, which is rare
    KeywordNoneoverride (optional but use it)

    Which operators can and cannot be overloaded

    GroupOperatorsNotes
    Arithmetic and bitwise+ - * / % ^ & | ~ << >> and all their = formsOverloadable. << and >> are also the stream operators.
    Comparison== != < <= > >= <=>Overloadable. Since C++20, <=> and == generate the rest.
    Increment and decrement++ --Overloadable. A dummy int parameter marks the postfix form.
    Access and call[] () -> ->* * &Overloadable. [], (), -> and = must be members.
    Other= , new delete, conversionsOverloadable, though , almost never should be.
    Cannot be overloaded. .* :: ?: sizeof alignof typeid and casts like static_castThe language reserves these. No workaround.
    Updated July 2026: Two rules changed recently and both save you code. C++20 added the three-way comparison operator <=>, so a single defaulted line replaces six hand-written comparison operators, and a != b is automatically rewritten as !(a == b). C++23 allows operator[] to take more than one argument, which finally removes the reason matrix classes used operator() for indexing. GCC has supported the multi-argument subscript since version 12; MSVC since 19.42.

    Member or non-member? The rule of thumb

    A member operator’s left operand is always your object, and it participates in no implicit conversions. A non-member operator treats both sides symmetrically, which matters more often than people expect.

    1. Must be a member: =, [], (), -> and conversion operators. The language says so.
    2. Must be a non-member: anything whose left operand is not your type. std::cout << obj has a stream on the left, so operator<< cannot be your member.
    3. Prefer a member: compound assignment (+=, -=), because it mutates the object and needs private access.
    4. Prefer a non-member: symmetric binary operators (+, -, ==), so that 3 * vec works as well as vec * 3.

    Use friend only when the non-member genuinely needs private data. If public accessors are enough, keep it a plain free function and your class stays smaller.

    A worked example: Vector2 from + through the stream operators

    This class shows the standard pattern. Implement += as a member, then define + as a non-member in terms of it, taking the left operand by value so the copy you need already exists.

    #include <iostream>
    #include <sstream>
    
    class Vector2 {
    public:
        Vector2() = default;
        Vector2(double x, double y) : x_(x), y_(y) {}
    
        double x() const { return x_; }
        double y() const { return y_; }
    
        // Compound assignment is the member; it mutates and returns a reference.
        Vector2& operator+=(const Vector2& rhs) {
            x_ += rhs.x_;
            y_ += rhs.y_;
            return *this;
        }
        Vector2& operator-=(const Vector2& rhs) {
            x_ -= rhs.x_;
            y_ -= rhs.y_;
            return *this;
        }
    
        // Unary minus: a new value, so return by value and mark it const.
        Vector2 operator-() const { return Vector2(-x_, -y_); }
    
    private:
        double x_{0.0};
        double y_{0.0};
    };
    
    // Binary + is a non-member written in terms of +=. Takes lhs by value.
    Vector2 operator+(Vector2 lhs, const Vector2& rhs) { return lhs += rhs; }
    Vector2 operator-(Vector2 lhs, const Vector2& rhs) { return lhs -= rhs; }
    
    // Scalar multiplication both ways, which only a non-member can do.
    Vector2 operator*(const Vector2& v, double k) { return Vector2(v.x() * k, v.y() * k); }
    Vector2 operator*(double k, const Vector2& v) { return v * k; }
    
    bool operator==(const Vector2& a, const Vector2& b) {
        return a.x() == b.x() && a.y() == b.y();
    }
    bool operator!=(const Vector2& a, const Vector2& b) { return !(a == b); }
    
    // Stream operators must be non-member: the left operand is the stream.
    std::ostream& operator<<(std::ostream& os, const Vector2& v) {
        return os << '(' << v.x() << ", " << v.y() << ')';
    }
    std::istream& operator>>(std::istream& is, Vector2& v) {
        double x = 0.0, y = 0.0;
        if (is >> x >> y) v = Vector2(x, y);
        return is;
    }
    
    int main() {
        Vector2 a(1.5, 2.0), b(0.5, -1.0);
    
        std::cout << "a + b   = " << a + b << '\n';
        std::cout << "a - b   = " << a - b << '\n';
        std::cout << "-a      = " << -a << '\n';
        std::cout << "a * 2   = " << a * 2 << '\n';
        std::cout << "3 * b   = " << 3 * b << '\n';
    
        a += b;
        std::cout << "after a += b: " << a << '\n';
        std::cout << "a == b ? " << std::boolalpha << (a == b) << '\n';
    
        std::istringstream in("7 -8");
        Vector2 parsed;
        in >> parsed;
        std::cout << "parsed  = " << parsed << '\n';
    }
    a + b   = (2, 1)
    a - b   = (1, 3)
    -a      = (-1.5, -2)
    a * 2   = (3, 4)
    3 * b   = (1.5, -3)
    after a += b: (2, 1)
    a == b ? false
    parsed  = (7, -8)

    Note the return types. operator+= returns Vector2& so that (a += b) += c works like it does for built-in types. operator+ returns Vector2 by value, because the result is a new object. Returning a reference from operator+ is a dangling-reference bug that compiles cleanly. The stream operators return the stream by reference, which is what makes chaining work. Writing the file version of that is the same skill applied to writing a file in C++ with ofstream.

    Subscript with const and non-const overloads

    Two overloads of operator[] differing only in constness is the standard pattern. The non-const version returns a mutable reference so obj[i] = x works; the const version returns a const reference so a const object can still be read.

    Recommended for you:

    How to Use Average True Range (ATR): Beginner’s Guide
    Blog·Jul 30, 2026

    How to Use Average True Range (ATR): Beginner’s Guide

    #include <initializer_list>
    #include <iostream>
    #include <stdexcept>
    #include <string>
    #include <vector>
    
    class Record {
    public:
        Record(std::initializer_list<std::string> fields) : fields_(fields) {}
    
        // Non-const overload: usable on the left of an assignment.
        std::string& operator[](std::size_t i) {
            if (i >= fields_.size()) throw std::out_of_range("Record index");
            return fields_[i];
        }
        // Const overload: returns a reference you cannot write through.
        const std::string& operator[](std::size_t i) const {
            if (i >= fields_.size()) throw std::out_of_range("Record index");
            return fields_[i];
        }
    
        // A second overload keyed by name. Overload sets can mix parameter types.
        std::string& operator[](const std::string& name) {
            for (std::size_t i = 0; i < names_.size(); ++i)
                if (names_[i] == name) return fields_[i];
            throw std::out_of_range("no field named " + name);
        }
    
        void set_names(std::vector<std::string> n) { names_ = std::move(n); }
        std::size_t size() const { return fields_.size(); }
    
    private:
        std::vector<std::string> fields_;
        std::vector<std::string> names_;
    };
    
    int main() {
        Record r{"Dana", "Whitfield", "Austin TX"};
        r.set_names({"first", "last", "city"});
    
        r[2] = "Denver CO";                 // non-const overload
        std::cout << r["first"] << ' ' << r["last"] << " of " << r[2] << '\n';
    
        const Record& cr = r;
        std::cout << "const read: " << cr[0] << '\n';
    
        try {
            std::cout << r[99];
        } catch (const std::out_of_range& e) {
            std::cout << "caught: " << e.what() << '\n';
        }
    }

    Output: Dana Whitfield of Denver CO, then const read: Dana, then caught: Record index. operator[] must be a member, so a non-member subscript is not an option. If you want string-keyed lookup as well as integer indexing, add another overload to the set exactly as shown.

    operator() and the stateful callable

    Overloading the call operator turns objects into functions that carry state. This is what every std::sort comparator and every lambda actually is under the hood.

    #include <algorithm>
    #include <functional>
    #include <iostream>
    #include <vector>
    
    // operator() turns an object into a callable, and unlike a plain function
    // it can carry state. This is what std::sort comparators usually are.
    struct ScaleBy {
        double factor;
        double operator()(double x) const { return x * factor; }
    };
    
    struct CountingLess {
        mutable int comparisons = 0;
        bool operator()(int a, int b) const { ++comparisons; return a < b; }
    };
    
    int main() {
        ScaleBy half{0.5};
        std::cout << "half(9)  = " << half(9) << '\n';
    
        std::vector<int> v{5, 1, 4, 2, 3};
        CountingLess cmp;
        std::sort(v.begin(), v.end(), std::ref(cmp));
        std::cout << "sorted:";
        for (int x : v) std::cout << ' ' << x;
        std::cout << "\ncomparisons: " << cmp.comparisons << '\n';
    }

    That reports 12 comparisons for five elements on this build. The std::ref matters: std::sort takes the comparator by value, so without it you would count comparisons on a copy and read zero from the original.

    Prefix and postfix ++ and —

    The two forms have the same name, so C++ distinguishes them with an unnamed dummy int parameter on the postfix version. Nothing is ever passed in it.

    #include <iostream>
    
    class Counter {
    public:
        explicit Counter(int start = 0) : n_(start) {}
    
        // Prefix: increment, then return a reference to this object.
        Counter& operator++() { ++n_; return *this; }
        Counter& operator--() { --n_; return *this; }
    
        // Postfix: the unnamed int parameter is a tag, not a value.
        // Copy first, mutate, return the old copy by value.
        Counter operator++(int) { Counter old = *this; ++n_; return old; }
        Counter operator--(int) { Counter old = *this; --n_; return old; }
    
        int value() const { return n_; }
    
    private:
        int n_;
    };
    
    std::ostream& operator<<(std::ostream& os, const Counter& c) { return os << c.value(); }
    
    int main() {
        Counter c(10);
        std::cout << "++c yields " << ++c << ", c is now " << c << '\n';
        std::cout << "c++ yields " << c++ << ", c is now " << c << '\n';
        std::cout << "--c yields " << --c << ", c is now " << c << '\n';
        std::cout << "c-- yields " << c-- << ", c is now " << c << '\n';
    }
    Tip: Prefix returns a reference and costs nothing extra. Postfix has to copy the old value to return it, which is why ++it is the idiomatic form in loops over iterators. Implement postfix in terms of prefix as shown, never the other way around.

    Assignment and the copy-and-swap idiom

    If your class owns a raw resource, operator= has to handle self-assignment and stay exception-safe. Copy-and-swap does both, and it collapses copy assignment and move assignment into one function.

    #include <algorithm>
    #include <cstddef>
    #include <iostream>
    #include <utility>
    
    class Buffer {
    public:
        explicit Buffer(std::size_t n) : size_(n), data_(new int[n]{}) {}
    
        Buffer(const Buffer& other) : size_(other.size_), data_(new int[other.size_]) {
            std::copy(other.data_, other.data_ + other.size_, data_);
        }
    
        Buffer(Buffer&& other) noexcept { swap(other); }
    
        // Copy-and-swap: one operator= handles both copy and move assignment.
        // The parameter is taken by value, so the copy (or move) already happened.
        Buffer& operator=(Buffer other) noexcept {
            swap(other);
            return *this;
        }
    
        ~Buffer() { delete[] data_; }
    
        void swap(Buffer& other) noexcept {
            std::swap(size_, other.size_);
            std::swap(data_, other.data_);
        }
    
        int& operator[](std::size_t i) { return data_[i]; }
        const int& operator[](std::size_t i) const { return data_[i]; }
        std::size_t size() const { return size_; }
    
    private:
        std::size_t size_{0};
        int*        data_{nullptr};
    };
    
    int main() {
        Buffer a(3);
        a[0] = 7; a[1] = 8; a[2] = 9;
    
        Buffer b(1);
        b = a;                      // copy assignment via copy-and-swap
        std::cout << "b: " << b[0] << ' ' << b[1] << ' ' << b[2] << " (size " << b.size() << ")\n";
    
        Buffer c(1);
        c = std::move(a);           // move assignment, same operator=
        std::cout << "c: " << c[0] << ' ' << c[1] << ' ' << c[2] << '\n';
    
        a = a;                      // self-assignment is safe by construction
        std::cout << "self-assignment survived\n";
    }

    The return *this; is not optional. Leave it out and the code will not compile, but the subtler version of this mistake is returning a copy instead of a reference, which compiles and quietly breaks assignment chaining. In modern code, of course, you would hold a std::vector<int> and write no special members at all.

    Conversion operators and explicit

    A conversion operator has no return type in its declaration, because the name supplies it. Making it implicit is convenient and frequently a mistake.

    #include <iostream>
    #include <string>
    
    class Celsius {
    public:
        explicit Celsius(double d) : deg_(d) {}
    
        // Implicit conversion to double. Convenient and dangerous.
        operator double() const { return deg_; }
    
        // Explicit conversion: needs static_cast or a direct-init context.
        explicit operator std::string() const { return std::to_string(deg_) + " C"; }
    
        // explicit operator bool is the one implicit-ish exception: it works in
        // if(), while() and ! without a cast, but not in arithmetic.
        explicit operator bool() const { return deg_ != 0.0; }
    
    private:
        double deg_;
    };
    
    int main() {
        Celsius t(21.5);
    
        double d = t;                                   // implicit, no cast needed
        std::cout << "as double: " << d << '\n';
    
        auto s = static_cast<std::string>(t);            // explicit, cast required
        std::cout << "as string: " << s << '\n';
    
        if (t) std::cout << "non-zero temperature\n";   // contextual conversion
    
        Celsius zero(0.0);
        std::cout << "zero is " << (zero ? "truthy" : "falsy") << '\n';
    
        // The hazard: because operator double is implicit, this compiles and
        // silently does floating point arithmetic on a temperature.
        std::cout << "t + 1 = " << t + 1 << '\n';
    }

    The last line prints t + 1 = 22.5. Adding a bare 1 to a temperature is almost certainly not what anyone meant, and the implicit conversion made it legal. Mark conversions explicit unless you have a strong reason not to. explicit operator bool is the exception worth knowing: it still works in conditions, which is exactly how if (!file) works on a stream.

    C++20: the spaceship operator replaces six functions

    Writing ==, !=, <, <=, > and >= by hand was six chances to get something wrong. Since C++20, one defaulted operator<=> generates the ordering operators, and a defaulted operator== generates equality. Members are compared in declaration order.

    #include <compare>
    #include <iostream>
    #include <set>
    #include <string>
    
    struct Version {
        int major{};
        int minor{};
        int patch{};
    
        // One line replaces ==, !=, <, <=, > and >=.
        // Members are compared in declaration order.
        auto operator<=>(const Version&) const = default;
        bool operator==(const Version&) const = default;
    };
    
    // When you need custom ordering, write <=> by hand and return the category.
    class Money {
    public:
        Money(long dollars, int cents) : cents_(dollars * 100 + cents) {}
    
        std::strong_ordering operator<=>(const Money& rhs) const {
            return cents_ <=> rhs.cents_;
        }
        bool operator==(const Money& rhs) const { return cents_ == rhs.cents_; }
    
        long total_cents() const { return cents_; }
    
    private:
        long cents_;
    };
    
    std::ostream& operator<<(std::ostream& os, const Money& m) {
        return os << '$' << m.total_cents() / 100 << '.'
                  << (m.total_cents() % 100 < 10 ? "0" : "") << m.total_cents() % 100;
    }
    
    int main() {
        Version a{1, 4, 0}, b{1, 12, 0};
        std::cout << std::boolalpha;
        std::cout << "a < b  : " << (a < b) << '\n';
        std::cout << "a == a : " << (a == a) << '\n';
        std::cout << "a >= b : " << (a >= b) << '\n';
    
        // Because <=> exists, std::set works with no extra comparator.
        std::set<Version> versions{{2, 0, 0}, {1, 4, 0}, {1, 12, 3}};
        for (const Version& v : versions)
            std::cout << v.major << '.' << v.minor << '.' << v.patch << ' ';
        std::cout << '\n';
    
        Money cheap(4, 99), pricey(12, 50);
        std::cout << cheap << " < " << pricey << " : " << (cheap < pricey) << '\n';
    
        auto cat = cheap <=> pricey;
        std::cout << "is less: " << (cat < 0) << ", is equal: " << (cat == 0) << '\n';
    }
    a < b  : true
    a == a : true
    a >= b : false
    1.4.0 1.12.3 2.0.0 
    $4.99 < $12.50 : true
    is less: true, is equal: false

    Note that the std::set sorted the versions numerically (1.4.0 before 1.12.3) with no comparator supplied, which is the practical payoff. The three return categories are std::strong_ordering when equal values are indistinguishable, std::weak_ordering when equivalent values can still differ (case-insensitive strings, for instance), and std::partial_ordering for types with incomparable values like floating point NaN. The spec is on the cppreference comparison operators page.

    C++23: operator[] with multiple arguments

    Before C++23, operator[] was limited to exactly one argument, which is why matrix classes have always used m(row, col). That restriction is gone.

    #include <iostream>
    #include <stdexcept>
    #include <vector>
    
    class Matrix {
    public:
        Matrix(std::size_t rows, std::size_t cols)
            : rows_(rows), cols_(cols), cells_(rows * cols, 0.0) {}
    
        // C++23: operator[] can take more than one argument.
        double& operator[](std::size_t r, std::size_t c) {
            check(r, c);
            return cells_[r * cols_ + c];
        }
        const double& operator[](std::size_t r, std::size_t c) const {
            check(r, c);
            return cells_[r * cols_ + c];
        }
    
        // The pre-C++23 workaround: operator() with two arguments.
        double& operator()(std::size_t r, std::size_t c) { return (*this)[r, c]; }
    
    private:
        void check(std::size_t r, std::size_t c) const {
            if (r >= rows_ || c >= cols_) throw std::out_of_range("Matrix subscript");
        }
        std::size_t rows_, cols_;
        std::vector<double> cells_;
    };
    
    int main() {
        Matrix m(2, 3);
        m[0, 0] = 1.0;
        m[1, 2] = 6.5;
        m(0, 1) = 2.25;
    
        for (std::size_t r = 0; r < 2; ++r) {
            for (std::size_t c = 0; c < 3; ++c) std::cout << m[r, c] << '\t';
            std::cout << '\n';
        }
    }

    Compiled with g++ -std=c++23 it prints 1 2.25 0 then 0 0 6.5. Under -std=c++20 the same file fails with “must have exactly one argument”, so keep the operator() form around if you support older standards.

    Rules and pitfalls worth taking seriously

    Preserve expected semantics. A reader who sees a + b assumes it produces a new value and leaves a alone, that == is symmetric and transitive, and that + is commutative for arithmetic-like types. Break those and your class becomes unusable with the standard algorithms and unreadable to everyone else.

    Get the return types right. Compound assignment and = return T&; binary arithmetic returns T by value; comparisons return bool; stream operators return the stream by reference. Prefix increment returns T&, postfix returns T.

    Be const-correct. Any operator that does not modify the object should be a const member, or take its parameters by const& if it is a non-member. Miss this and your operators simply do not exist for const objects, producing a confusing “no match” error at the call site.

    Never overload &&, || or ,. The built-in versions short-circuit and impose an evaluation order. An overloaded version is an ordinary function call, so both operands are always evaluated and the guaranteed sequencing disappears. Code that relied on p && p->f() being safe will crash.

    Do not overload for cleverness. If the meaning is not obvious from the symbol, use a named function. A Path class overloading / to join components is good because it reads exactly like a path. A Logger overloading % to mean “format” is not.

    Troubleshooting the errors you will actually see

    Two mistakes account for most of the confusion, so here they are, running.

    Recommended for you:

    Cats Pretend They Don’t Care. Science Just Proved They Remember Everything.
    Blog·Jul 28, 2026

    Cats Pretend They Don’t Care. Science Just Proved They Remember Everything.

    #include <iostream>
    
    // Pitfall 1: operator== that calls itself instead of comparing members.
    struct Broken {
        int n;
        bool operator==(const Broken& rhs) const {
            static int depth = 0;
            if (++depth > 3) {
                std::cout << "  bailed out at depth " << depth << '\n';
                return false;
            }
            std::cout << "  entered operator== at depth " << depth << '\n';
            return *this == rhs;                 // BUG: same operator, forever
        }
    };
    
    struct Fixed {
        int n;
        bool operator==(const Fixed& rhs) const { return n == rhs.n; }
    };
    
    // Pitfall 2: operator<< as a member puts the object on the left.
    struct WrongStream {
        int n;
        std::ostream& operator<<(std::ostream& os) const { return os << n; }
    };
    
    struct RightStream { int n; };
    std::ostream& operator<<(std::ostream& os, const RightStream& r) { return os << r.n; }
    
    int main() {
        std::cout << "Broken ==:\n";
        Broken a{1}, b{1};
        (void)(a == b);
    
        Fixed c{1}, d{1};
        std::cout << "Fixed  ==: " << std::boolalpha << (c == d) << '\n';
    
        WrongStream w{5};
        w << std::cout;                 // compiles; std::cout << w does not
        std::cout << " <- printed by the member form\n";
    
        RightStream r{5};
        std::cout << r << " <- printed by the non-member form\n";
    }
    Broken ==:
      entered operator== at depth 1
      entered operator== at depth 2
      entered operator== at depth 3
      bailed out at depth 4
    Fixed  ==: true
    5 <- printed by the member form
    5 <- printed by the non-member form

    “no match for operator<<“. You wrote operator<< as a member. Replacing std::cout << w in the program above with the member form gives exactly that error, because member lookup only considers the left operand’s type, and the left operand is a stream you do not own. Move it out of the class and give it two parameters.

    Infinite recursion in ==. Writing return *this == rhs; inside operator== calls the function you are writing. Without the depth guard above, that is a stack overflow. Compare members, or just write = default and let the compiler do it.

    Ambiguity errors. Usually caused by an implicit conversion operator plus a converting constructor, which gives the compiler two equally good paths. Mark one explicit. In C++20, another new source is a hand-written operator< coexisting with a defaulted <=>; pick one strategy per class.

    Forgetting return *this. Any operator declared to return T& has to. Compilers warn about the missing return, but the version that silently misbehaves is declaring T instead of T& and returning a copy.

    Frequently asked questions

    Can you override an operator in C++?

    Not in the usual sense. C++ supports operator overloading, which means defining what an operator does for your own type. Overriding applies only to virtual member functions. An operator can technically be virtual, and then it can be overridden, but that is rare and usually a design smell.

    Which operators must be member functions in C++?

    Assignment =, subscript [], function call (), member access ->, and conversion operators. Everything else can be a non-member. The stream operators << and >> must be non-members when the left operand is a stream.

    Why does my operator<< not work as a member function?

    Because a member operator’s left operand is your object, so a member operator<< defines obj << stream, not stream << obj. Declare it outside the class as std::ostream& operator<<(std::ostream&, const T&), adding friend only if it needs private members.

    What does the int parameter in operator++(int) mean?

    Nothing, semantically. It is a tag that lets the compiler tell the postfix form apart from the prefix form, since both would otherwise have identical signatures. Leave it unnamed. No caller ever supplies a value for it.

    Do I still need to write operator!= in C++20?

    No. Given operator==, the compiler rewrites a != b as !(a == b) automatically. Similarly, a defaulted operator<=> supplies <, <=, > and >=. You still declare == separately, because <=> does not generate it.

    Should I use friend or a plain free function for operator overloads?

    Use a plain free function when public accessors give you everything you need, since it keeps the class interface smaller and reduces coupling. Use friend when the operator legitimately needs private state and adding a public getter would leak implementation detail.

    Wrapping up

    The pattern that covers most real classes is short. Put += and friends in the class returning T&, define + outside it in terms of +=, write operator<< as a non-member, and let auto operator<=>(const T&) const = default; handle comparison unless you need something custom. Mark every conversion explicit and stop before you get clever.

    The failure mode nobody warns you about is overloading too much. Each operator you define is a claim that the symbol means the obvious thing for your type. When it does, the code reads beautifully; a Money class with + and <=> is genuinely better than one with add() and compareTo(). When it does not, you have built a puzzle. Being able to justify that line in review, or in an interview, is worth as much as the code itself, which is the whole subject of explaining your coding solution in an interview.

    Next steps in this series: giving your class a stream operator pairs naturally with storing text file data into a vector in C++, and a custom comparator is exactly what powers a case-insensitive string comparison. Operator semantics are also one of the topics that reward a full book over a blog post, and if your next project happens to be front-end rather than systems, we keep a similar shortlist of the best Vue.js books to learn.

    Share. Facebook Twitter Pinterest LinkedIn Tumblr Telegram Email Copy Link
    Previous ArticleHow to Use Average True Range (ATR): Beginner’s Guide
    Next Article How to Compare Two Strings Ignoring Case in C++
    Ethan Caldwell

      Ethan Caldwell is GeekBlog's resident Apple specialist, covering the entire Apple ecosystem - iPhone, iPad, Mac, Apple Watch, AirPods and the software that ties them together. A longtime iOS user and gadget collector, Ethan tracks Cupertino's every move, breaking down Apple keynotes, A- and M-series chip benchmarks, iOS feature updates and the rumor mill into clear, practical takes that help readers decide whether the latest Apple hardware is worth the upgrade.

      Related Posts

      18 Mins Read

      New York vs Florida: Which State Is Better to Move To?

      15 Mins Read

      What State Is Best to Invest in Real Estate in 2026?

      15 Mins Read

      Texas vs California: Which State Is Better in 2026?

      17 Mins Read

      How to Explain Your Coding Solution in an Interview

      13 Mins Read

      Massachusetts vs Kentucky: Which State Is Better?

      12 Mins Read

      Washington vs Indiana: Which State Is Better to Live In?

      Top Posts

      Best Stores for Buying MP3 and Digital Music You Can Keep Forever (2026)

      August 2, 202521 Views

      How to Fix PS5 Controller Stick Drift (2026): 7 Working Methods

      July 10, 202612 Views

      Every iPhone Camera Ranked in 2026 (Best to Worst)

      July 6, 20269 Views
      Stay In Touch
      • Facebook

      Subscribe to Updates

      Get the latest tech news from FooBar about tech, design and biz.

      Most Popular

      Best Stores for Buying MP3 and Digital Music You Can Keep Forever (2026)

      August 2, 2025918 Views

      Discord will require a face scan or ID for full access next month

      February 9, 2026770 Views

      Trade in your old phone and get up to $1,100 off a new iPhone 17 at AT&T – here’s how

      September 10, 2025382 Views
      Our Picks

      Meta Wants an AI Agent Managing Your Life. Wall Street Isn’t So Sure

      August 2, 2026

      MakuluLinux’s New AI-OS Wants to Run Your Whole Desktop, Not Just Answer Questions

      August 1, 2026

      AI Tokens Got 98% Cheaper. Corporate AI Bills Are Exploding Anyway

      July 31, 2026

      Subscribe to Updates

      Get the latest creative news from FooBar about art, design and business.

      Facebook
      • About Us
      • Contact us
      • Privacy Policy
      • Disclaimer
      • Terms and Conditions
      © 2026 GeekBlog

      Type above and press Enter to search. Press Esc to cancel.