Practice Lab 22 — Device session guard with custom deleters¶
Difficulty: Advanced · Estimated time: 5–6 hours · Module 11: RAII, Dynamic Memory and Smart Pointers
The lab where the resource is not memory. Everything you learned about ownership has to be re-expressed for a handle, a lock and a counter.
At a glance¶
| Difficulty | Advanced |
| Estimated time | 5–6 hours |
| Module | Module 11 — RAII, Dynamic Memory and Smart Pointers |
| Concepts practised | RAII for non-memory resources, custom deleters, unique_ptr with a deleter, noexcept destructors, move semantics of handles, cleanup ordering |
| Prerequisite | Practice Lab 21 |
| Class design | yours — with the resource's protocol specified |
Scenario¶
A lab instruments a device through a C-style library. The library's protocol is:
handle open_device(const char* name); // returns 0 on failure
int read_sample(handle h); // -1 on error
void close_device(handle h); // must be called exactly once
Handles are not pointers, and closing twice is a fault that bricks the device until it is power-cycled.
The lab's current code calls close_device in the happy path and forgets it on two error paths, and a
device was bricked last month.
You cannot change the library. You can change how your program uses it.
Problem statement¶
Build a safe wrapper around the library, and demonstrate that no path can close twice or forget to close.
- A RAII handle type that owns a
handlevalue and closes it in its destructor, move-only, with a deleted copy. - A factory function that opens a device and returns the owning type, or reports failure without leaking.
- A custom deleter demonstration: the same ownership expressed with
std::unique_ptrand a deleter — then compare it with the hand-written RAII type, in the notes, on: lines of code, move semantics, and what a reader has to know. - A session type that owns a handle and reads samples, with three exit paths: normal, early return, and an exception thrown from inside the session.
- A counter of open handles, so the count at exit proves correctness, plus a count of close calls per handle, which must never exceed one.
- A device registry that owns several sessions and can hand out references to them without transferring ownership.
In raii-notes.md:
- The three exit paths' counts, for both the hand-written type and the
unique_ptrversion. - Which one you would ship, and the cost of the other.
- The rule for destructors, and what would break it (name one thing a destructor must not do here).
- The handle's "close exactly once" rule, and the two mechanisms in your program that enforce it.
Learning objectives¶
By the end of this lab you will be able to:
- Wrap a C-style resource in an RAII type so that acquisition and release happen exactly once.
- Write a custom deleter and use it with
std::unique_ptr, and compare it honestly with a hand-written type. - Explain why a destructor must not throw, using this resource's protocol as the example.
- Ensure cleanup on every exit path, including exceptions, without remembering to write it.
- Design a registry that hands out access without transferring ownership.
Functional requirements¶
- The RAII handle is move-only; a moved-from handle is inert and safe to destroy.
- The factory returns a valid handle or reports failure, and a failed open leaks nothing (the count is zero after failure).
close_deviceis called exactly once per opened handle, whatever the path — the per-handle close count proves it.- An exception thrown inside a session leaves no device open.
- Reading from a moved-from or invalid handle is refused, with a documented result.
- The handle type's destructor does not throw, and the notes say what it does instead if the library could fail.
- The registry owns its sessions and hands out references; destroying the registry closes every device.
- The custom-deleter version behaves identically in counts, and differs only in how it is written.
- The final counts at exit: open devices 0, and every per-handle close count exactly 1.
- No
close_devicecall appears anywhere outside the two wrappers — searched for and recorded.
Constraints¶
- The library's interface is given and must not be modified: simulate it in a source file with the exact signatures above, narrating each call.
- The RAII type and the
unique_ptrversion must both exist and both be exercised. - No bare
close_devicecalls inmainor in the session type. - Destructors must be
noexcept(they are by default — do not add athrowinside one). - No warnings, including no unused-handle warnings.
Starter task¶
In raii-notes.md, before coding:
- Write the RAII type's five special members: which you declare, which you delete, and which you let the compiler write.
- Write the factory's failure behaviour: what it returns, and what the caller must do with it (can the caller ignore it? should the type make ignoring impossible?).
- Write the deleter for the
unique_ptrversion, and predict the two versions' counts for the three paths. - Write down the thing a destructor must never do, and find one place in your design where that rule is load-bearing.
Implementation tasks¶
- Implement the simulated library with narration.
- Implement the RAII handle with move semantics and the close counter.
- Implement the factory and demonstrate the failure path.
- Implement the
unique_ptrversion with a deleter and run the same paths. - Implement the session and the registry, with the three exit paths.
- Run everything and record the counts; compare with the predictions.
- Search for
close_device(and record every call site.
Test cases¶
| # | Action | Expected result |
|---|---|---|
| 1 | Open a valid device | Handle valid; open count 1 |
| 2 | Open an unknown device | Failure reported; open count 0 after the attempt |
| 3 | Destroy a handle | close_device called once |
| 4 | Move a handle | The target owns it; the source is inert; still one close |
| 5 | Early return from a session | Close called once |
| 6 | Exception thrown inside a session | Close called once; nothing open at exit |
| 7 | Read from a moved-from handle | Refused, with the documented result |
| 8 | Two sessions in a registry, destroy the registry | Two closes, in a documented order |
| 9 | Registry hands out a reference | No ownership transfer; no extra close |
| 10 | Per-handle close counts | Every opened handle closed exactly once; none twice |
| 11 | unique_ptr version, all three paths |
Identical counts to the hand-written version |
| 12 | Search for close_device( |
Only inside your two wrappers |
Boundary cases to run deliberately: a registry with no devices; an exception thrown before the wrapper is constructed; opening the same device twice (decide whether that is allowed, and record the rule).
Expected behaviour¶
[lib] open_device("spectrometer-1") -> handle 7
[raii] acquired handle 7
[lib] read_sample(7) -> 42
[raii] early return taken
[lib] close_device(7) (close count for 7: 1)
open devices at exit: 0 closes per handle: all exactly 1
exception path:
[lib] open_device("spectrometer-2") -> handle 8
[raii] acquired handle 8
[lib] read_sample(8) -> -1 (error)
throwing: read failed
[lib] close_device(8) (close count for 8: 1)
open devices at exit: 0
unique_ptr version: identical counts on all three paths
close_device call sites: raii_handle.cpp:2, unique_ptr_handle.cpp:1
Extension challenge¶
Support two devices at once, where the second must be opened only after the first, and where opening the second can fail.
Record in raii-notes.md:
- What happens to the first device when the second fails, with the counts.
- How the ownership of two resources is expressed (two members, a pair, a composite wrapper), and which you chose.
- What changes if the two devices must be closed in the reverse of their opening order, and how your design guarantees it.
- The change cost: files, operations, lines.
Debugging challenge¶
The device is bricked by a double close:
struct Session {
handle device;
~Session() { close_device(device); }
void finish() { close_device(device); } // "close early, it is polite"
};
Session s{open_device("spectrometer-1")};
s.finish();
// destructor runs at the end of the scope
Reproduce it — have the simulated library count closes per handle and print a fault on the second — and answer:
- Which two call sites closed the same handle, and in what order?
- Why is the destructor's call not the bug, even though it is the one that faults?
- Rewrite
finishso that the "polite early close" is expressed as ownership being released, and say what the destructor then does. - Which test case in the table catches this, and what did your design do to make it impossible?
Reflection questions¶
- Your RAII type owns a handle value, not a pointer. What does that tell you about RAII being "about pointers"?
- The
unique_ptrversion and the hand-written one behaved identically. Which is clearer, and what does a reader need to know to use each? - A destructor must not throw. Which operation in this library could fail, and what does your destructor do instead?
- The registry hands out references. What lifetime guarantee is the caller relying on, and where is it written down?
- A moved-from handle is inert. What exactly does it do on destruction, and where is that documented?
- If the library's
close_devicecould block for a second, what would change about your design, and what would you not want to do inside a destructor?
Completion checklist¶
- [ ] It builds from a documented command with
-Wall -Wextra -Werrorand no warnings - [ ] The RAII handle is move-only, and a moved-from handle is safe to destroy
- [ ] A failed open leaks nothing — count printed
- [ ] Every opened handle is closed exactly once, with per-handle counts printed
- [ ] The exception path closes cleanly
- [ ] The custom-deleter version exists and matches the hand-written counts
- [ ]
close_deviceappears only inside the wrappers — search recorded - [ ] Destructors do not throw, and the notes say what they do instead
- [ ] All twelve test cases run, with output recorded
- [ ] The debugging challenge was explained before being fixed
- [ ]
raii-notes.mdstates which wrapper I would ship, and why
Related: Lab index · Module 11 — RAII and Smart Pointers · Previous: Practice Lab 21 · Next: Practice Lab 23 — Generic repository for two record kinds