Skip to content

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.

  1. A RAII handle type that owns a handle value and closes it in its destructor, move-only, with a deleted copy.
  2. A factory function that opens a device and returns the owning type, or reports failure without leaking.
  3. A custom deleter demonstration: the same ownership expressed with std::unique_ptr and 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.
  4. 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.
  5. 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.
  6. 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_ptr version.
  • 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

  1. The RAII handle is move-only; a moved-from handle is inert and safe to destroy.
  2. The factory returns a valid handle or reports failure, and a failed open leaks nothing (the count is zero after failure).
  3. close_device is called exactly once per opened handle, whatever the path — the per-handle close count proves it.
  4. An exception thrown inside a session leaves no device open.
  5. Reading from a moved-from or invalid handle is refused, with a documented result.
  6. The handle type's destructor does not throw, and the notes say what it does instead if the library could fail.
  7. The registry owns its sessions and hands out references; destroying the registry closes every device.
  8. The custom-deleter version behaves identically in counts, and differs only in how it is written.
  9. The final counts at exit: open devices 0, and every per-handle close count exactly 1.
  10. No close_device call 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_ptr version must both exist and both be exercised.
  • No bare close_device calls in main or in the session type.
  • Destructors must be noexcept (they are by default — do not add a throw inside one).
  • No warnings, including no unused-handle warnings.

Starter task

In raii-notes.md, before coding:

  1. Write the RAII type's five special members: which you declare, which you delete, and which you let the compiler write.
  2. 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?).
  3. Write the deleter for the unique_ptr version, and predict the two versions' counts for the three paths.
  4. Write down the thing a destructor must never do, and find one place in your design where that rule is load-bearing.

Implementation tasks

  1. Implement the simulated library with narration.
  2. Implement the RAII handle with move semantics and the close counter.
  3. Implement the factory and demonstrate the failure path.
  4. Implement the unique_ptr version with a deleter and run the same paths.
  5. Implement the session and the registry, with the three exit paths.
  6. Run everything and record the counts; compare with the predictions.
  7. 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:

  1. Which two call sites closed the same handle, and in what order?
  2. Why is the destructor's call not the bug, even though it is the one that faults?
  3. Rewrite finish so that the "polite early close" is expressed as ownership being released, and say what the destructor then does.
  4. Which test case in the table catches this, and what did your design do to make it impossible?

Reflection questions

  1. Your RAII type owns a handle value, not a pointer. What does that tell you about RAII being "about pointers"?
  2. The unique_ptr version and the hand-written one behaved identically. Which is clearer, and what does a reader need to know to use each?
  3. A destructor must not throw. Which operation in this library could fail, and what does your destructor do instead?
  4. The registry hands out references. What lifetime guarantee is the caller relying on, and where is it written down?
  5. A moved-from handle is inert. What exactly does it do on destruction, and where is that documented?
  6. If the library's close_device could 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 -Werror and 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_device appears 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.md states 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