Operator overloading
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:
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}
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
| Moneyno 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:
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}
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:
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}
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:
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}
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:
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}
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:
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}
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.
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}
a + b returns a new Point (11, 22), and + a on that gives (12, 24). + goes before <<, so what's shown is the finished sum.
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 it | Don'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 sort | a 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.
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.
7:45 9:15 9:30
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}
Show a fraction
✓Write << for Fraction so it shows as top/bottom. The program should show Each share is 3/4.
Each share is 3/4
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}
Fix it: the change
✓The program doesn't compile because Money can't subtract. Add operator- so the program shows Change: 750 cents.
Change: 750 cents
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}
What you learned
- An operator is a function with a special name:
a + bisa.operator+(b). To use operators on your own type, write them; without one you getno match for 'operator+'. +changes neither side: aconstreference parameter, aconstfunction, a new object returned.+=changes itself: noconst, and it returns a reference to*this.privateworks 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::sortneeds no third argument, andsetandmapcan use the type as a key. - Write
<<as a function outside the class and declare it afriendinside; it returnsstd::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.