Kodewick
Lesson 17 of 30Contents

Operator overloading

Lesson 17 of 30 · Part 4 · Classes · about 15 minutes

Store money as a whole number of cents and you never get decimal rounding errors. Write a Money class, and the natural way to add two amounts is a + b. But the compiler doesn't know how to add two Moneys:

main.cpp
1#include <iostream>2 3class Money {4public:5    Money(int cents) : cents_(cents) {}6 7    int cents() const {8        return cents_;9    }10 11private:12    int cents_;13};14 15int main() {16    Money a{250};17    Money b{199};18    Money total = a + b;19    std::cout << total.cents() << '\n';20    return 0;21}
Output
main.cpp: In function 'int main()':
main.cpp:18:21: error: no match for 'operator+' (operand types are 'Money' and 'Money')
   18 |     Money total = a + b;
      |                   ~ ^ ~
      |                   |   |
      |                   |   Money
      |                   Money

no match for 'operator+': there's no + that works on two Moneys. + for int, double and std::string is already written; for your own type, you tell the compiler what + means. This is operator overloading.

Overloading +

An operator is just a function with a special name: + is operator+. Write it as a member function:

main.cpp
1#include <iostream>2 3class Money {4public:5    Money(int cents) : cents_(cents) {}6 7    Money operator+(const Money& other) const {8        return Money{cents_ + other.cents_};9    }10 11    int cents() const {12        return cents_;13    }14 15private:16    int cents_;17};18 19int main() {20    Money a{250};21    Money b{199};22    Money total = a + b;23    std::cout << total.cents() << '\n';24    return 0;25}
Output
449

a + b is a.operator+(b): a on the left is the object it's called on, and b on the right is the parameter other. + changes neither a nor b, so the parameter is a const reference, the function itself gets const, and it returns a new Money.

other.cents_ is a private member of b. Why is it allowed? Because private works per class, not per object: Money's member functions can use the private members of any Money. Press Step and see that when you enter operator+, other refers to b.

+= changes itself

+= is different from +: it changes the object on the left. So it doesn't get const, and by habit it returns a reference to itself:

main.cpp
1#include <iostream>2 3class Money {4public:5    Money(int cents) : cents_(cents) {}6 7    Money& operator+=(const Money& other) {8        cents_ += other.cents_;9        return *this;10    }11 12    int cents() const {13        return cents_;14    }15 16private:17    int cents_;18};19 20int main() {21    Money wallet{1000};22    wallet += Money{250};23    wallet += Money{75};24    std::cout << wallet.cents() << '\n';25    return 0;26}
Output
1325

*this means "this object itself" (this is a pointer to it; more on pointers in lesson 19). Money{250} makes an amount on the spot, and it's gone once the adding is done.

== and !=

When are two fractions equal? 1/2 and 2/4 have different tops and bottoms, but they're the same number. You decide what == means: if the cross products are equal, the fractions are equal. Write != using ==, so there's nothing to think through twice:

main.cpp
1#include <iostream>2 3class Fraction {4public:5    Fraction(int num, int den) : num_(num), den_(den) {}6 7    bool operator==(const Fraction& other) const {8        return num_ * other.den_ == other.num_ * den_;9    }10 11    bool operator!=(const Fraction& other) const {12        return !(*this == other);13    }14 15private:16    int num_;17    int den_;18};19 20int main() {21    Fraction half{1, 2};22    Fraction two_quarters{2, 4};23    Fraction third{1, 3};24    std::cout << (half == two_quarters) << '\n';25    std::cout << (half != third) << '\n';26    return 0;27}
Output
1
1

Mind the parentheses around (half == two_quarters): << goes before ==, so without them it becomes (std::cout << half) == ..., which doesn't compile.

Note: Writing == doesn't give you != for free (it does from C++20); < and > must each be written too: writing < doesn't give you >. Write each one you want.

< and sorting

With no third argument, std::sort uses <. Write operator< for your type and you can sort it directly, with no comparison function:

main.cpp
1#include <algorithm>2#include <iostream>3#include <string>4#include <vector>5 6class Song {7public:8    Song(const std::string& title, int seconds) : title_(title), seconds_(seconds) {}9 10    bool operator<(const Song& other) const {11        return seconds_ < other.seconds_;12    }13 14    void show() const {15        std::cout << title_ << " " << seconds_ << "s\n";16    }17 18private:19    std::string title_;20    int seconds_;21};22 23int main() {24    std::vector<Song> list{{"Morning", 269}, {"Way Home", 265}, {"Summer Night", 226}};25    std::sort(list.begin(), list.end());26    for (const auto& s : list) {27        s.show();28    }29    return 0;30}
Output
Summer Night 226s
Way Home 265s
Morning 269s

Sorted from shortest to longest. A std::set<Song>, or a std::map with Song keys, uses this < for its order too; without it, they can't hold a Song.

<< so cout can show an object

Calling cents() and adding the decimal point by hand every time gets old; you'd rather write std::cout << price. But the left side of << is std::cout, not a Money, so it can't be a member function of Money. Instead, write it as an ordinary function whose two parameters are the left and right sides, and declare it a friend inside the class so it may use private members:

main.cpp
1#include <iostream>2 3class Money {4public:5    Money(int cents) : cents_(cents) {}6 7    friend std::ostream& operator<<(std::ostream& out, const Money& m);8 9private:10    int cents_;11};12 13std::ostream& operator<<(std::ostream& out, const Money& m) {14    out << "$" << m.cents_ / 100 << ".";15    if (m.cents_ % 100 < 10) {16        out << "0";17    }18    out << m.cents_ % 100;19    return out;20}21 22int main() {23    Money price{1205};24    Money fee{90};25    std::cout << "Price: " << price << '\n';26    std::cout << "Shipping: " << fee << '\n';27    return 0;28}
Output
Price: $12.05
Shipping: $0.90

std::ostream is the type of output streams like std::cout. The function returns out at the end so that std::cout << "Price: " << price << '\n' can keep going: the result of std::cout << price is std::cout again.

Guess first

What does this show?

1#include <iostream>2 3class Point {4public:5    Point(int x, int y) : x_(x), y_(y) {}6 7    Point operator+(const Point& other) const {8        return Point{x_ + other.x_, y_ + other.y_};9    }10 11    friend std::ostream& operator<<(std::ostream& out, const Point& p);12 13private:14    int x_;15    int y_;16};17 18std::ostream& operator<<(std::ostream& out, const Point& p) {19    out << "(" << p.x_ << ", " << p.y_ << ")";20    return out;21}22 23int main() {24    Point a{1, 2};25    Point b{10, 20};26    std::cout << a + b + a << '\n';27    return 0;28}

When is it worth it?

Overloading operators is about making code read naturally. There's one rule: make it do what anyone would guess.

Worth itDon't
+ - for money, fractions, points+ to add a song to a playlist (add is clearer)
== for things that can be equal (with !=)== that compares only some members, silently
< for things with a natural order, so they sorta forced < when the order isn't obvious (use a comparison function, lesson 14)
<< for types you want to show<< that also changes the object on the side

When in doubt, write a member function with a clear name.

Practice

Write the code, then press Check. A hint costs nothing; the answer is there when you need it.

Exercise 1

Times in order

Write operator< for Time: the earlier hour comes first, and for the same hour, compare minutes. Then std::sort works with no third argument.

It should print
7:45
9:15
9:30
exercise1.cpp
1#include <algorithm>2#include <iostream>3#include <vector>4 5class Time {6public:7    Time(int hours, int minutes) : hours_(hours), minutes_(minutes) {}8 9    void show() const {10        std::cout << hours_ << ":" << minutes_ << '\n';11    }12 13private:14    int hours_;15    int minutes_;16};17 18int main() {19    std::vector<Time> alarms{{9, 30}, {7, 45}, {9, 15}};20    std::sort(alarms.begin(), alarms.end());21    for (const auto& t : alarms) {22        t.show();23    }24    return 0;25}
Output
Press Run or Ctrl+Enter
Exercise 2

Show a fraction

Write << for Fraction so it shows as top/bottom. The program should show Each share is 3/4.

It should print
Each share is 3/4
exercise2.cpp
1#include <iostream>2 3class Fraction {4public:5    Fraction(int num, int den) : num_(num), den_(den) {}6 7private:8    int num_;9    int den_;10};11 12int main() {13    Fraction share{3, 4};14    std::cout << "Each share is " << share << '\n';15    return 0;16}
Output
Press Run or Ctrl+Enter
Exercise 3

Fix it: the change

The program doesn't compile because Money can't subtract. Add operator- so the program shows Change: 750 cents.

It should print
Change: 750 cents
exercise3.cpp
1#include <iostream>2 3class Money {4public:5    Money(int cents) : cents_(cents) {}6 7    int cents() const {8        return cents_;9    }10 11private:12    int cents_;13};14 15int main() {16    Money paid{2000};17    Money price{1250};18    Money change = paid - price;19    std::cout << "Change: " << change.cents() << " cents\n";20    return 0;21}
Output
Press Run or Ctrl+Enter

What you learned

  • An operator is a function with a special name: a + b is a.operator+(b). To use operators on your own type, write them; without one you get no match for 'operator+'.
  • + changes neither side: a const reference parameter, a const function, a new object returned. += changes itself: no const, and it returns a reference to *this.
  • private works per class: a member function can use the private members of another object of the same class.
  • Write == and != separately, != in terms of ==: !(*this == other).
  • With operator<, std::sort needs no third argument, and set and map can use the type as a key.
  • Write << as a function outside the class and declare it a friend inside; it returns std::ostream& so << can keep going.
  • Overload an operator only when its meaning is obvious; when in doubt, write a member function with a clear name.