Skip to content

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:

  1. Money-like amounts in whole pence (reuse your Lab 9 type or write a small one).
  2. InvoiceLine — a value type: description, quantity, unit price. Copyable, comparable, printable.
  3. Invoice — an identity type: a number, a customer name, a date, and a collection of lines.
  4. Not copyable: the copy constructor and copy assignment are deleted, and a comment says why.
  5. Movable: it can be built and moved into a container.
  6. Lines can be added, removed and printed; the total is computed and consistent.
  7. Payment — a move-only type holding an owned resource (simulate it with a dynamically allocated buffer and a destructor, or an owned string buffer).
  8. 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

  1. InvoiceLine copies and compares as a value; two lines with equal fields compare equal.
  2. Invoice refuses to be copied — at compile time — and moving one leaves the source empty and valid.
  3. An invoice number is unique across the desk; a duplicate is refused at creation.
  4. An invoice's total equals the sum of its lines, and this is checked in the program after every change.
  5. Payment cannot be copied; it can be moved into the desk's record, and it releases its resource exactly once.
  6. A payment records the invoice number it applies to and the amount; a payment for an unknown invoice is refused.
  7. Payment amounts never exceed the invoice total plus a documented tolerance (choose zero unless you have a reason).
  8. The day's summary prints: invoices created, lines, totals, payments and the outstanding balance, with the balance equal to total minus payments.
  9. A container operation that moves (for example push_back of a payment) compiles and works; an operation that would copy does not compile, and the message is recorded.
  10. 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 Payment must 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:

  1. 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.
  2. Predict the compiler message for copying an invoice, and for copying a payment. Write your prediction; then capture the real message and compare.
  3. Predict what happens to a std::vector<Payment> when it reallocates, in terms of copies and moves.
  4. Write the one-sentence test for "should this type be copyable?" that you will use from now on.

Implementation tasks

  1. Implement InvoiceLine and the amount type.
  2. Implement Invoice with deleted copies and working moves; verify with a container.
  3. Implement Payment as move-only with a real resource.
  4. Implement the desk: creation, numbering, payments, balance.
  5. Write main with the test cases, including the two deliberate copy attempts whose compiler messages you must capture.
  6. 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:

  1. Why does this not compile in your design, and why is that the correct outcome rather than an inconvenience?
  2. Which two objects did the old design create, and how did the numbering end up duplicated?
  3. 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.
  4. Which test case in the table catches a duplicate number?

Reflection questions

  1. Which of your four types was hardest to classify, and what decided it?
  2. Deleting the copy constructor produced a compile error. What did the error message tell a reader about the design intent?
  3. A Payment is move-only because it owns a resource. What happens to std::vector<Payment> when it reallocates, and why must the move be noexcept for that to be efficient?
  4. Where in your code does the rule of zero still apply, and what would break it?
  5. 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.
  6. 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 -Werror and no warnings
  • [ ] semantics.md classifies 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
  • [ ] Payment owns 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