Why this assignment
This assignment uses every idea in the module in one program. You will write a small bank that reads commands from the Input box and prints the result of each one. The bank has two kinds of account, savings and checking, which share most of their behaviour and differ in the rules for withdrawals and interest. The bank can take a snapshot of all its accounts and restore them later, which forces you to copy objects correctly.
Nothing here is new on its own. Classes and private data come from lesson 1. Constructors and the this pointer come from lesson 2. Inheritance and access levels come from lesson 3. Virtual functions, override and abstract classes come from lesson 4. The static counter and the copy rules come from lesson 5. The assignment asks you to combine them, and that combination is what interviewers mean when they ask you to design a small system.
Work through it in order. Get the first few commands working before you touch the snapshot. A program that handles three commands correctly is worth more than one that handles ten commands incorrectly.
The brief
A village co-operative keeps its accounts in a text file that a clerk types into a program each day. The clerk types one command per line. The program answers each command with one line of output. Two kinds of account exist.
A savings account pays interest once a month at a rate in whole percent. It cannot go below zero, so a withdrawal larger than the balance is refused.
A checking account can go below zero by up to an overdraft limit that is set when the account is opened. A withdrawal that would take the balance below the negative limit is refused. Checking accounts earn no interest.
The bank also keeps a snapshot. A clerk can save the whole state of the bank, then make changes, and then restore the saved state if a mistake was made. The restore must bring back the saved state exactly, and changes made after the restore must not reach the saved copy. That last rule is the reason the assignment is harder than it looks.
The bank must also count how many accounts were ever opened. The count is a fact about the history of the program, so it does not change when accounts are closed, restored or copied. It is a static member of the account class.
The commands
Every command is one word, followed by its arguments, separated by spaces. Each command prints one or more lines. The table below lists every command and the exact text it must print. Your output must match this text character for character, because the grader compares it exactly.
| Command | Arguments | What it does | Output when it works | Output when it fails |
|---|---|---|---|---|
open | NAME, KIND, PARAM | Opens an account. KIND is savings (PARAM is the interest rate) or checking (PARAM is the overdraft limit). | opened NAME | error: NAME exists or error: unknown type |
deposit | NAME, AMOUNT | Adds money to an account. | ok | error: bad amount or error: no account NAME |
withdraw | NAME, AMOUNT | Takes money out, if the account's rule allows it. | ok | error: bad amount, error: no account NAME or error: insufficient funds |
interest | none | Adds interest to every savings account. Checking accounts are not changed. | ok | not applicable |
report | none | Prints one line per account, in the order they were opened. | NAME KIND BALANCE for each account | not applicable |
count | none | Prints how many accounts were ever opened. | N opened | not applicable |
snapshot | none | Saves a copy of all accounts, replacing any earlier snapshot. | ok | not applicable |
restore | none | Replaces the accounts with a copy of the saved snapshot. The snapshot is kept, so you can restore again. | ok | error: no snapshot |
Any other first word prints error: unknown command. A line that is blank is ignored, and the program stops when there is no more input.
Two rules apply to the amounts. Every amount is a whole number. A deposit or a withdrawal of zero or less prints error: bad amount. The check for a bad amount comes before the check for the account, so a bad amount on an unknown account prints the bad-amount message.
An example run
The sample input below is a full run. Type it into the Input box, or into a file, and compare your output line by line with the sample output. Work out each line on paper before you run the program, because that is the fastest way to find a mistake in your reasoning.
Sample input:
open alice savings 5
open bob checking 100
deposit alice 1000
deposit bob 50
withdraw bob 120
withdraw bob 200
withdraw alice 2000
interest
report
open alice checking 0
snapshot
deposit alice 1
withdraw bob 1000
restore
report
count
bogusSample output:
opened alice
opened bob
ok
ok
ok
error: insufficient funds
error: insufficient funds
ok
alice savings 1050
bob checking -70
error: alice exists
ok
ok
error: insufficient funds
ok
alice savings 1050
bob checking -70
2 opened
error: unknown commandTrace the middle of the run, because it tests the important rules. Bob has 50 in a checking account with a limit of 100. Withdrawing 120 takes him to -70, which is above the negative limit of -100, so the withdrawal is allowed and prints ok. Withdrawing 200 more would take him to -270, which is below -100, so it is refused. Alice has 1000 in savings, and withdrawing 2000 is more than she has, so it is refused.
The interest command gives Alice 5 percent of 1000, which is 50, so she has 1050. Bob's checking account is not changed. The report shows both balances.
The second open for the name alice is refused, because the name exists, and the failed open does not change the count. The snapshot stores Alice at 1050 and Bob at -70. Then Alice receives 1 more, so she has 1051. The withdrawal of 1000 from Bob is refused. The restore brings the snapshot back, so the report shows Alice at 1050 again. The count is 2, because only two accounts were ever opened successfully.
Edge cases your program must handle
Test each of these cases yourself. The grader uses tests like them, and most lost marks come from one of these.
- A withdrawal of exactly the balance from a savings account is allowed and leaves zero.
- A withdrawal that reaches the overdraft limit exactly from a checking account is allowed. A checking account with a limit of 100 and a balance of 0 may withdraw 100 and reach -100.
- A withdrawal one more than the limit is refused.
- Interest on a zero balance is zero, and interest on a savings account with rate 0 changes nothing.
- A deposit of zero or a negative number prints
error: bad amount. - A restore with no snapshot prints
error: no snapshotand changes nothing. - Two restores in a row both print
okand both bring back the snapshot, because the snapshot is kept. - A change after a snapshot does not change the snapshot. Test this directly: snapshot, deposit, restore, and check the balance.
- Empty input prints nothing at all.
- An unknown kind of account prints
error: unknown type, and the account is not added and not counted.
What you must build
Your program must follow the design below. The grader reads the code as well as the output, because the design is what you are being tested on.
- An abstract base class
Accountwith a private balance, a name, and a virtual destructor. -
Accounthas pure virtual functions for the kind name, the withdrawal rule and the copy (clone). -
Accounthas a virtualapplyInterestthat does nothing by default. - A static counter inside
Accountcounts the accounts opened, and a static function returns it. - A class
Savingsderives publicly fromAccount, with its own rate, and overrides every pure virtual function. - A class
Checkingderives publicly fromAccount, with its own overdraft limit, and overrides every pure virtual function. - Every override is written with the
overridekeyword. - Only
Savingsearns interest, through its override ofapplyInterest. - A class
Bankowns its accounts throughunique_ptr, not through raw pointers. -
snapshotmakes a deep copy of every account by callingcloneon each one. -
restoremakes a new deep copy from the snapshot, so the snapshot stays unchanged. - Every command prints exactly the text in the table.
- The program runs with the address sanitiser and reports no memory errors.
- The code compiles with
g++ -std=c++17 -Wall -Wextraand produces no warnings.
Grading rubric
Your grade depends on what the program does and how it is built. The three levels below describe what a solution at each mark looks like. Read them before you start, and check your program against the higher level before you submit it.
| Mark | What the solution does | A typical failure at this level |
|---|---|---|
| 60% | Opens, deposits, withdraws, reports and prints unknown command for a bad word. Uses a class with a private balance. Gets the messages right for the simple cases. | Interest, snapshot or count are missing, or the program prints extra text. |
| 80% | Handles every command in the table with the right messages. Savings and checking have different withdrawal rules and different interest rules through virtual functions. The static count is correct. Destructors are virtual and the program frees everything. | The restore shares accounts with the snapshot, or the count changes after a failed open. |
| 100% | Everything at 80%, plus snapshot and restore as true deep copies made through clone. Passes every edge case in the list above. Compiles with no warnings under -Wall -Wextra, runs clean under the address sanitiser, and writes override on every override. | Restore hands the backup's accounts to the bank, so a later deposit changes the snapshot. |
A useful self-check is to ask, for each command, "what would happen if this command were called twice?" Deposit twice doubles the money, withdraw twice may fail the second time, and restore twice should bring back the same state both times. Thinking this way catches most design mistakes.
How to check your work
Compile with warnings turned on, so the compiler points out the mistakes it can see:
g++ -std=c++17 -Wall -Wextra -o bank bank.cppRun it on the sample input and compare the output with the sample output above. A quick way to compare is to save your output to a file and read the two files side by side.
Then run it under the address sanitiser to find memory errors:
g++ -std=c++17 -g -fsanitize=address -o bank_asan bank.cpp
./bank_asan < sample.txtThe sanitiser prints nothing when the program is clean. If it prints a report, it names the line of the leak or the double free. A leak report at the end means some account was not destroyed, which usually points at a missing virtual on a destructor or a raw pointer that was never deleted.
Hints
Try the assignment without hints first. If you are stuck for more than half an hour on one command, open the first hint, and then the next, only as far as you need.
Hint 1: start with one account type
Write Account and Savings first, with open, deposit, withdraw and report. Do not write the bank's snapshot yet. Make sure those four commands print the exact text in the table before you add anything else. Then add Checking, and then interest, count, snapshot and restore one at a time.
Hint 2: put the rule in a virtual function
Each kind of account decides whether a withdrawal is allowed. Give Account a pure virtual canWithdraw(int amount) const, and let Savings and Checking each write their own rule. Then withdraw in the base class can be a single line of logic: ask canWithdraw, and if it says yes, subtract the amount. This is the template method pattern, and it keeps the balance logic in one place.
Hint 3: make the snapshot with clone
A clone method that returns unique_ptr<Account> lets each derived class copy itself. In Savings, the body can be return make_unique<Savings>(*this);. The bank then builds a new vector by calling clone on each account in the old vector. For restore, build a new vector the same way from the backup, so the backup is not handed over. Remember that unique_ptr cannot be copied, so you must build the new vector element by element.
Hint 4: check the order of the checks
Read the whole command before you decide what to print. For deposit and withdraw, read the name and the amount. Then check the amount first, because the rule says the bad-amount message comes before the account check. Only then look up the account. A failed open must not add to the count, so increment the counter in the constructor only, and make sure the code only builds the object after the name check has passed.
Reference solution
Try to finish your own version first. Compare it with this reference only when you have something working, and read the parts you did differently, because a different design is not always wrong. The reference uses one static counter, virtual functions for the rules, clone for copies and unique_ptr for ownership.
Reference solution
#include <iostream>
#include <memory>
#include <string>
#include <utility>
#include <vector>
using namespace std;
class Account {
string name_;
int balance_ = 0;
static inline int opened_ = 0;
protected:
void addToBalance(int delta) { balance_ += delta; }
public:
explicit Account(string name) : name_(name) { opened_++; }
virtual ~Account() {}
const string& name() const { return name_; }
int balance() const { return balance_; }
static int openedCount() { return opened_; }
virtual string kind() const = 0;
virtual bool canWithdraw(int amount) const = 0;
virtual void applyInterest() {}
virtual unique_ptr<Account> clone() const = 0;
void deposit(int amount) { balance_ += amount; }
bool withdraw(int amount) {
if (!canWithdraw(amount)) return false;
balance_ -= amount;
return true;
}
};
class Savings : public Account {
int ratePercent_;
public:
Savings(string name, int rate) : Account(name), ratePercent_(rate) {}
string kind() const override { return "savings"; }
bool canWithdraw(int amount) const override {
return amount > 0 && amount <= balance();
}
void applyInterest() override {
addToBalance(balance() * ratePercent_ / 100);
}
unique_ptr<Account> clone() const override {
return make_unique<Savings>(*this);
}
};
class Checking : public Account {
int overdraft_;
public:
Checking(string name, int limit) : Account(name), overdraft_(limit) {}
string kind() const override { return "checking"; }
bool canWithdraw(int amount) const override {
return amount > 0 && balance() - amount >= -overdraft_;
}
unique_ptr<Account> clone() const override {
return make_unique<Checking>(*this);
}
};
class Bank {
vector<unique_ptr<Account>> accounts_;
vector<unique_ptr<Account>> backup_;
bool hasBackup_ = false;
static vector<unique_ptr<Account>> cloneAll(const vector<unique_ptr<Account>>& src) {
vector<unique_ptr<Account>> out;
for (const auto& a : src) out.push_back(a->clone());
return out;
}
public:
Account* find(const string& name) const {
for (const auto& a : accounts_) {
if (a->name() == name) return a.get();
}
return nullptr;
}
void add(unique_ptr<Account> a) { accounts_.push_back(move(a)); }
void applyInterest() {
for (auto& a : accounts_) a->applyInterest();
}
void report() const {
for (const auto& a : accounts_) {
cout << a->name() << " " << a->kind() << " " << a->balance() << "\n";
}
}
void snapshot() {
backup_ = cloneAll(accounts_);
hasBackup_ = true;
}
bool restore() {
if (!hasBackup_) return false;
accounts_ = cloneAll(backup_);
return true;
}
};
int main() {
Bank bank;
string cmd;
while (cin >> cmd) {
if (cmd == "open") {
string name, kind;
int param = 0;
cin >> name >> kind >> param;
if (bank.find(name) != nullptr) {
cout << "error: " << name << " exists\n";
} else if (kind == "savings") {
bank.add(make_unique<Savings>(name, param));
cout << "opened " << name << "\n";
} else if (kind == "checking") {
bank.add(make_unique<Checking>(name, param));
cout << "opened " << name << "\n";
} else {
cout << "error: unknown type\n";
}
} else if (cmd == "deposit" || cmd == "withdraw") {
string name;
int amount = 0;
cin >> name >> amount;
Account* acc = bank.find(name);
if (amount <= 0) {
cout << "error: bad amount\n";
} else if (acc == nullptr) {
cout << "error: no account " << name << "\n";
} else if (cmd == "deposit") {
acc->deposit(amount);
cout << "ok\n";
} else if (acc->withdraw(amount)) {
cout << "ok\n";
} else {
cout << "error: insufficient funds\n";
}
} else if (cmd == "interest") {
bank.applyInterest();
cout << "ok\n";
} else if (cmd == "report") {
bank.report();
} else if (cmd == "count") {
cout << Account::openedCount() << " opened\n";
} else if (cmd == "snapshot") {
bank.snapshot();
cout << "ok\n";
} else if (cmd == "restore") {
cout << (bank.restore() ? "ok" : "error: no snapshot") << "\n";
} else {
cout << "error: unknown command\n";
}
}
return 0;
}Walk through the reference once after you have your own version working. Notice three choices. The static counter is incremented in the base constructor, so a copy made by clone does not change the count, which is the rule the spec asks for. The clone method returns make_unique<Savings>(*this), which uses the compiler's copy constructor for Savings. That works here because Savings holds only a string and integers, which copy deeply on their own. Finally, restore builds a brand-new vector from the backup, so the backup is never handed to the bank. Each choice protects one of the rules in the rubric.
Extension for a stretch
If you finish early and want more practice, add a transfer NAME1 NAME2 AMOUNT command. It must withdraw from the first account and deposit into the second, and it must refuse the whole transfer if the withdrawal is refused, so that no money is lost or created. Decide the message for each failure, and add tests for the case where the second account does not exist. The stretch tests whether you can keep two changes together without leaving the bank half updated.
What you now know
- You can design a small system from a written brief, choosing classes, data and virtual functions.
- You can make an abstract base class with pure virtual functions that each kind of account implements.
- You can keep money rules private inside the base class, and let each derived class decide what is allowed.
- You can make deep copies of polymorphic objects with a
clonefunction andunique_ptr. - You can check a program with compiler warnings and the address sanitiser, and read their messages.
- You have used every idea in the module together, and you can explain each design choice in an interview.