Skip to content

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:

  1. how the value is stored, and why that representation beats storing a double;
  2. which operators the type needs, and for each: member or free function;
  3. how you enforce "never negative" without an exception yet (you are only up to Module 5);
  4. 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.

struct Record {
    std::string title;
    int         year;
    std::string author;
};

Task. Write a refactor plan that defines one ordering for Record, used everywhere. Your plan must:

  1. state the tie-break order (title, then year, then author — or your own, defended);
  2. show the single comparison that implements it, so std::sort and the three screens all agree;
  3. explain what happens to two records that are equal on all three fields, and why that matters to sorting;
  4. 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