Course outline

C++ / Advanced / Lesson 15

Resource safety across ownership boundaries

Adapt external ownership to RAII, specify deletion and observation rules, and protect resource lifetimes when callbacks escape their creating scope.

About 5 min readContent reviewed
On this page

Lesson goals

  • Use a custom deleter with an explicit matching release policy.
  • Keep deleter state and borrowed resources alive long enough.
  • Choose between retained ownership and weak observation for callbacks.

Match acquisition and release exactly

External APIs may return handles that require a special release function rather than delete. Wrap such a handle in a small RAII type or a smart pointer with the matching deleter immediately after successful acquisition. Verify whether null means failure, whether release can fail, and whether the API imposes thread or order restrictions. Never mix malloc with delete or new with free. A wrapper is correct only when it preserves the original API contract; merely placing a raw address inside a smart pointer does not establish the right cleanup semantics.

Make cleanup dependencies visible

The first example uses a synthetic heap resource and a stateful deleter that increments an external release counter. The counter is declared before the owner and therefore outlives it. The unique_ptr constructor receives the allocation immediately, and the deleter performs only a bounded integer increment and deletion. Production wrappers should avoid throwing during cleanup. A stateful deleter is part of the owner object, so its own borrowed dependencies require review. This example deliberately uses new with delete as a matching pair; a real C handle would need its own release function.

cpp A release policy with a live dependency
#include <iostream>
#include <memory>
struct Resource { int id; };
struct Release {
    int* closed;
    void operator()(Resource* resource) const noexcept {
        ++*closed;
        delete resource;
    }
};
int main() {
    int closed{};
    {
        std::unique_ptr<Resource, Release> owner(new Resource{7}, Release{&closed});
        std::cout << owner->id << "\n";
    }
    std::cout << closed << "\n";
}
Expected output
7
1

The resource is read while owned and released once at scope exit. closed stays alive until after the deleter finishes.

Stored work extends dependencies

A callback stored for later execution outlives the call that registered it. Capturing a unique_ptr transfers exclusive ownership into a move-only closure, which cannot be stored in C++20 std::function when copying is required. Capturing shared_ptr retains the resource until every owning callback is gone. Capturing weak_ptr allows expiration and requires a checked lock at execution time. These are different product behaviors, not interchangeable optimizations. Decide whether cancellation releases resources immediately or whether queued work is allowed to retain them, and document the queue lifetime and destruction policy.

Lock a weak observer for the whole operation

The observer example returns optional<int>, distinguishing a live value from expiration without a dangling access. lock creates a temporary shared owner held for the complete read. Checking expired and then dereferencing a raw pointer would leave a gap in which another owner could disappear. The first call sees a live value; after the explicit shared owner leaves scope, the closure remains but its weak capture does not retain the integer. This example is single-threaded, yet the same ownership pattern is useful when execution timing is asynchronous.

cpp Expiration is an explicit result
#include <functional>
#include <iostream>
#include <memory>
#include <optional>
int main() {
    std::function<std::optional<int>()> observe;
    {
        auto owner = std::make_shared<int>(42);
        observe = [weak = std::weak_ptr<int>{owner}]() -> std::optional<int> {
            if (auto locked = weak.lock()) { return *locked; }
            return std::nullopt;
        };
        std::cout << observe().value() << "\n";
    }
    std::cout << (observe().has_value() ? "live" : "expired") << "\n";
}
Expected output
42
expired

The weak capture never owns the integer. The temporary locked owner protects the read, and the later call reports expiration.

Review all completion paths

Resource-safe code handles normal completion, early returns, exceptions, cancellation, and partial construction. If a constructor throws, fully constructed member objects are destroyed, while the enclosing object destructor does not run. Put each acquired resource into an owning member promptly so partial construction remains safe. For multi-resource operations, decide which effects can roll back and which need compensation. Test release counts with synthetic handles, then use appropriate memory and thread diagnostics on realistic executions. Also inspect raw-pointer escape points: a clean leak report does not prove that every borrowed access has a valid lifetime.

Common errors and fixes

A deleter accesses a dead counter.

Why it happens
Its borrowed state died before the owning handle.
How to fix it
Reorder or own deleter state so it outlives cleanup.
Prevent it next time
Include deleter dependencies in the ownership review.

A weak observer uses a stale raw pointer.

Why it happens
An expiration check was separated from acquiring ownership.
How to fix it
Use lock and retain its shared_ptr during access.
Prevent it next time
Treat the lock result as the lifetime guard.

A C resource crashes during smart-pointer cleanup.

Why it happens
The default deleter does not match the acquisition API.
How to fix it
Provide the documented release operation as a deleter.
Prevent it next time
Record acquisition and release as an inseparable pair.

Check your understanding

1. Change the observer capture from weak_ptr to shared_ptr. Explain the lifetime after the original owner leaves scope and what must release the final ownership.

Show worked solution

The stored closure now owns the integer, so it remains live after the original owner is destroyed. Clearing or destroying observe releases the captured shared_ptr; if no other owners remain, the integer is destroyed then. This may be the intended retained-work contract, but it no longer models optional observation of a resource that can expire independently.

2. A constructor opens two resources, and acquiring the second throws. Explain a safe member layout and which destructors run.

Show worked solution

Store each resource in an RAII member as soon as acquisition succeeds, in dependency order. If construction of the second member fails, the first completed member is destroyed automatically. The enclosing object never finished construction, so its destructor is not called. Keeping the first resource only in an unmanaged raw variable would make cleanup much harder to guarantee.

Key takeaways

  • Match release policies and keep deleter dependencies alive.
  • Stored callbacks are lifetime decisions.
  • Review partial construction and cancellation as normal paths.

Official references