Practice Lab 20 — Invoice copy semantics¶
Difficulty: Advanced · Estimated time: 5 hours · Module 10: Copying, Assignment and Resource Management
Not every type should be copyable. This lab is about deciding, per type, what copying means — and about the three answers: copy it properly, copy nothing, or move it.
At a glance¶
| Difficulty | Advanced |
| Estimated time | 5 hours |
| Module | Module 10 — Copying and Resource Management |
| Concepts practised | Value types versus identity types, deleted copy, move-only types, resource-owning members, the rule of zero in a composed class, explicit, immutability |
| Prerequisite | Practice Lab 19 |
| Class design | yours — with three copying decisions specified by requirement |
Scenario¶
A billing system produces invoices. Each invoice has a number, a customer, a date, and a list of lines. A line has a description, a quantity and a unit price.
Three requirements force three different answers to "what does copying mean?":
- An invoice's customer and line are values: copying one is fine and expected.
- An invoice's number is an identity: two invoices with the same number is a mistake the type should prevent, so an invoice is not copyable.
- A payment against an invoice holds a resource (a receipt file), so it is movable but not copyable.
A developer's earlier version made everything copyable, and the system issued the same invoice number twice in one month.
Problem statement¶
Design and implement:
Money-like amounts in whole pence (reuse your Lab 9 type or write a small one).InvoiceLine— a value type: description, quantity, unit price. Copyable, comparable, printable.Invoice— an identity type: a number, a customer name, a date, and a collection of lines.- Not copyable: the copy constructor and copy assignment are deleted, and a comment says why.
- Movable: it can be built and moved into a container.
- Lines can be added, removed and printed; the total is computed and consistent.
Payment— a move-only type holding an owned resource (simulate it with a dynamically allocated buffer and a destructor, or an owned string buffer).- A billing desk that creates invoices, adds lines, records payments, and prints a day's summary.
In semantics.md:
- For each of the four types, which special members exist: copy, move, both, neither — and the sentence that forced it.
- The compiler messages you get if you try to copy an invoice and if you try to copy a payment. Capture them; they are the reason this design is enforced rather than documented.
- Where the rule of zero applies in your code, and where it does not.
Learning objectives¶
By the end of this lab you will be able to:
- Decide whether a type is a value, an identity, or a resource owner, and express that in its special members.
- Delete copy operations deliberately, and read the compiler's message when somebody copies anyway.
- Write a move-only type, and use it as a member of another class without breaking that class's moves.
- Explain why a container of invoices needs them to be movable but not copyable.
- Recognise the case where the right answer is "no copy constructor, and no need for one".
Functional requirements¶
InvoiceLinecopies and compares as a value; two lines with equal fields compare equal.Invoicerefuses to be copied — at compile time — and moving one leaves the source empty and valid.- An invoice number is unique across the desk; a duplicate is refused at creation.
- An invoice's total equals the sum of its lines, and this is checked in the program after every change.
Paymentcannot be copied; it can be moved into the desk's record, and it releases its resource exactly once.- A payment records the invoice number it applies to and the amount; a payment for an unknown invoice is refused.
- Payment amounts never exceed the invoice total plus a documented tolerance (choose zero unless you have a reason).
- The day's summary prints: invoices created, lines, totals, payments and the outstanding balance, with the balance equal to total minus payments.
- A container operation that moves (for example
push_backof a payment) compiles and works; an operation that would copy does not compile, and the message is recorded. - Every type that owns a resource releases it exactly once — proven by narration counts.
Constraints¶
- Header and source per type family, with a documented build command.
- The resource in
Paymentmust be a real allocation with a real destructor — not a comment. - Deleted operations must be deleted, not left undefined, so that the failure is a compile-time error with a clear message.
- No smart pointers yet (Module 11 will convert
Payment), but state in the notes what the conversion would remove. - No warnings.
Starter task¶
In semantics.md, before coding:
- Classify each of the four types: value, identity, or resource owner. For each, write the special members you will declare and the sentence that forces it.
- Predict the compiler message for copying an invoice, and for copying a payment. Write your prediction; then capture the real message and compare.
- Predict what happens to a
std::vector<Payment>when it reallocates, in terms of copies and moves. - Write the one-sentence test for "should this type be copyable?" that you will use from now on.
Implementation tasks¶
- Implement
InvoiceLineand the amount type. - Implement
Invoicewith deleted copies and working moves; verify with a container. - Implement
Paymentas move-only with a real resource. - Implement the desk: creation, numbering, payments, balance.
- Write
mainwith the test cases, including the two deliberate copy attempts whose compiler messages you must capture. - Narrate and count destructions, and check the counts match the constructions.
Test cases¶
| # | Action | Expected result |
|---|---|---|
| 1 | Two equal lines compared | Equal |
| 2 | Add three lines to an invoice | Total equals the sum |
| 3 | Remove a line | Total drops by that line's amount |
| 4 | Copy an invoice | Does not compile; the message is captured |
| 5 | Move an invoice into a vector | Compiles; source empty and valid |
| 6 | Create a second invoice with the same number | Refused |
| 7 | Copy a payment | Does not compile; the message is captured |
| 8 | Move a payment into the desk | Accepted; resource ownership transferred |
| 9 | Record a payment for an unknown invoice | Refused |
| 10 | Record a payment exceeding the total | Refused |
| 11 | Day summary | Balance equals total minus payments |
| 12 | Destruction narration counts at exit | Constructions equal destructions, one each, for every type |
Boundary cases to run deliberately: an invoice with no lines and a zero total; a payment of 1 penny; a payment exactly equal to the total; moving an empty invoice.
Expected behaviour¶
[invoice] INV-0001 Ayesha Khan 20260414
[line] "Compilers" x1 @ 4500 = 4500
[line] "Shipping" x1 @ 500 = 500
total: 5000 pence (checked against lines: agree)
copy an invoice -> error: use of deleted function 'Invoice::Invoice(const Invoice&)'
move an invoice into a vector -> ok; source now has 0 lines
copy a payment -> error: use of deleted function 'Payment::Payment(const Payment&)'
move a payment into the desk -> ok; [resource] moved, owner now the desk
day summary: invoices 1, lines 2, total 5000, payments 1, outstanding 0
destructions: invoices 1, payments 1, resources released 1
Extension challenge¶
Add credit notes: a negative invoice that references the invoice it credits, and which must be impossible to copy for the same reason invoices are.
Record in semantics.md:
- Whether a credit note is an invoice (inheritance), or has one (composition), and the cost of each.
- How the desk prevents a credit note that exceeds the original.
- What the outstanding balance of the day means once credit notes exist.
- The change cost: files, operations, lines — and whether any existing special member had to change.
Debugging challenge¶
The billing desk issues the same invoice number twice, in a release that passed its tests:
Invoice BillingDesk::create_invoice(const std::string& customer) {
Invoice invoice{next_number_++, customer, today_};
invoices_.push_back(invoice); // "the desk keeps a copy"
return invoice; // "and returns one for the caller"
}
Answer, with evidence:
- Why does this not compile in your design, and why is that the correct outcome rather than an inconvenience?
- Which two objects did the old design create, and how did the numbering end up duplicated?
- Rewrite it so that one invoice exists, is owned in one place, and is available to the caller — and say what the caller may do with it.
- Which test case in the table catches a duplicate number?
Reflection questions¶
- Which of your four types was hardest to classify, and what decided it?
- Deleting the copy constructor produced a compile error. What did the error message tell a reader about the design intent?
- A
Paymentis move-only because it owns a resource. What happens tostd::vector<Payment>when it reallocates, and why must the move benoexceptfor that to be efficient? - Where in your code does the rule of zero still apply, and what would break it?
- An invoice number is an identity. What else in this system is an identity, and what else is a value? Answer for the customer name, the date, and the line.
- If invoices had to be copied for an archive, which of these designs would you choose: a clone operation with a new number, a separate snapshot type, or making invoices copyable? Give the cost of each.
Completion checklist¶
- [ ] It builds from a documented command with
-Wall -Wextra -Werrorand no warnings - [ ]
semantics.mdclassifies each type and states the forcing sentence - [ ] Both copy attempts fail to compile, and both messages are captured verbatim
- [ ] Invoice moves work, including into a container, leaving a valid empty source
- [ ]
Paymentowns a real resource, is move-only, and releases it exactly once - [ ] Invoice numbers are unique, and a duplicate is refused
- [ ] Totals are verified against the lines inside the program
- [ ] The day summary balances: total = payments + outstanding
- [ ] All test cases run, with output recorded; case 12's narration counts agree
- [ ] The debugging challenge was answered before any code was changed
- [ ] I can state the one-sentence test for whether a type should be copyable
Related: Lab index · Module 10 — Copying and Resource Management · Previous: Practice Lab 19 · Next: Practice Lab 21 — Smart pointer object manager