Bank 4 — Operators and static members¶
Eleven exercises on making a type speak the language the standard library already understands, and on class-level state. Module 5. Levels L1–L6.
Solutions are in Solutions and answers; nudges are in Hints. Attempt first.
Exercises¶
E36 — When is an operator a good citizen? L1 concept¶
Task. State the two rules an overloaded operator should follow to surprise nobody
who reads code that uses your type. Then answer: why is overloading operator+ to
subtract legal but bad, and what is the one number you should never overload
operator&& or operator, around?
E37 — Make the money add up L2 complete¶
#include <iostream>
class Money {
public:
explicit Money(long long cents) : cents_(cents) {}
// TODO: operator+ so that `a + b` yields a new Money whose cents_ is the sum
// TODO: an accessor cents() const
long long cents() const { return cents_; }
private:
long long cents_;
};
int main() {
Money price{1250}; // 12.50
Money tax{175}; // 1.75
Money total = price + tax;
std::cout << total.cents() << '\n';
}
Task. Supply operator+ and the accessor so the program prints 1425. Should
operator+ be a member or a free function here? Justify in one sentence.
E38 — Chaining L2 predict¶
#include <iostream>
class Vec2 {
public:
Vec2(double x, double y) : x_(x), y_(y) {}
Vec2 operator+(const Vec2& other) const {
return Vec2{ x_ + other.x_, y_ + other.y_ };
}
friend std::ostream& operator<<(std::ostream& os, const Vec2& v) {
return os << '(' << v.x_ << ", " << v.y_ << ')';
}
private:
double x_;
double y_;
};
int main() {
Vec2 a{1.0, 2.0};
Vec2 b{3.0, 4.0};
std::cout << a + b << '\n';
std::cout << "start " << a << " end\n";
}
Task. Write both output lines exactly. Then explain why operator<< must return
std::ostream& and not void for the second line to compile.
E39 — The queue machine L2 trace scenario¶
A single ticket machine hands out numbered tickets. Every ticket gets the next number, and the machine reports how many it has issued.
#include <iostream>
class Ticket {
public:
Ticket() : id_(++issued_) {}
int id() const { return id_; }
static int issuedCount() { return issued_; }
private:
int id_;
static int issued_;
};
int Ticket::issued_ = 100;
int main() {
std::cout << Ticket::issuedCount() << '\n';
Ticket a;
Ticket b;
Ticket c;
std::cout << a.id() << ' ' << b.id() << ' ' << c.id() << '\n';
std::cout << Ticket::issuedCount() << '\n';
}
Task. Three output lines. Explain why issued_ must be static and not simply a
member, and what would happen if the line int Ticket::issued_ = 100; were removed.
E40 — Make it sortable L3 modify scenario¶
A price list must be sorted by product name, and, when two products share a name, by
price. The type currently has no comparison operators, so std::sort cannot be used.
#include <string>
class Product {
public:
Product(std::string name, double price) : name_(name), price_(price) {}
std::string name() const { return name_; }
double price() const { return price_; }
private:
std::string name_;
double price_;
};
Task. Add the minimum operators needed to sort a std::vector<Product> with the
default std::sort. State which operator is the only one actually required, and why
adding only that one is correct even though it feels incomplete.
E41 — The assignment that cannot chain L3 debug¶
#include <iostream>
class Buffer {
public:
Buffer& operator=(const Buffer& other) {
std::cout << "assigned\n";
return *this;
}
};
int main() {
Buffer a;
Buffer b;
Buffer c;
a = b = c;
}
Task. This one does compile — so change it: rewrite operator= so that it
returns void, predict what the compiler now objects to on the a = b = c; line, and
then reproduce the real message with your toolchain. Why does the standard library
expect assignment to return a reference?
E42 — Before and after L2 predict¶
#include <iostream>
class Counter {
public:
Counter& operator++() { // prefix
++n_;
return *this;
}
Counter operator++(int) { // postfix
Counter old = *this;
++n_;
return old;
}
int n() const { return n_; }
private:
int n_ = 0;
};
int main() {
Counter c;
++c;
c++;
std::cout << c.n() << '\n';
Counter d = c++;
std::cout << c.n() << ' ' << d.n() << '\n';
}
Task. Write both output lines. What single argument distinguishes the postfix overload from the prefix one, and why is postfix usually the more expensive of the two?
E43 — Member or free function? L4 analyze¶
class Money {
public:
explicit Money(long long cents) : cents_(cents) {}
Money operator+(const Money& other) const {
return Money{ cents_ + other.cents_ };
}
long long cents() const { return cents_; }
private:
long long cents_;
};
A colleague wants Money half = 2 * m; to work as well as Money sum = m + 2; and is
surprised that the second one fails to compile.
Task. Explain precisely why a member operator+ blocks 2 * m, and why it
does not block m + 2 (hint: think about the implicit *this on the left). Then
state which form of operator+ fixes it and why that form is preferred for symmetric
operators.
E44 — A sort that crashes L4 debug¶
#include <algorithm>
#include <iostream>
#include <vector>
class Score {
public:
explicit Score(int points) : points_(points) {}
bool operator<(const Score& other) const {
return points_ <= other.points_; // look carefully
}
int points() const { return points_; }
private:
int points_;
};
int main() {
std::vector<Score> scores{ Score{3}, Score{1}, Score{2}, Score{2} };
std::sort(scores.begin(), scores.end());
for (const Score& s : scores) std::cout << s.points() << ' ';
}
Task. The operator "looks" fine. State what property a comparison used by sorting must have, how this one violates it, and what the consequences are (be precise about whether it is undefined behaviour). Then fix it and give the sorted output.
E45 — Design a Money type L5 design scenario¶
A payments desk handles cash amounts in a single currency. Amounts are added,
subtracted, compared, scaled by an integer, and printed as 12.50. Negative amounts
are not allowed to leave the type; construction with a negative value is a
programmer error.
Task. Decide and write down:
- how the value is stored, and why that representation beats storing a
double; - which operators the type needs, and for each: member or free function;
- how you enforce "never negative" without an exception yet (you are only up to Module 5);
- which operators you deliberately did not provide, and why.
How you will be judged. On the storage decision and on the restraint — a design that overloads everything is not a design.
E46 — One ordering to rule them all L6 refactor scenario¶
A catalogue sorts records by several fields, and three different functions each compare two records by hand with slightly different rules. The result is that the sorted order is inconsistent between screens.
Task. Write a refactor plan that defines one ordering for Record, used
everywhere. Your plan must:
- state the tie-break order (title, then year, then author — or your own, defended);
- show the single comparison that implements it, so
std::sortand the three screens all agree; - explain what happens to two records that are equal on all three fields, and why that matters to sorting;
- name one screen whose order legitimately differs, and how you would express that without duplicating the rule.
Success. One place defines "less than"; everywhere else refers to it.
Previous topic: Constructors and lifecycle · Next topic: Inheritance · Hints: hints.md · Solutions: solutions.md