C++ / a working model

37 / 163   ·   C++14   ·   10 min

RAII: Give Cleanup Responsibility to Objects

Keep this sentence

RAII encapsulates resource acquisition and release inside owning objects, using a determined destructor timing to handle both normal returns and exception unwinding. The key is not that “resources must live on the stack,” but that every resource has a clear owner: construction failure does not leak, and destructors do not throw failures further outward.

In this lesson
  1. Cleanup Should Belong to an Object
  2. Who Recovers Resources When Construction Fails
  3. Destructors Finish Cleanup; They Do Not Report the Main Error
  4. Example
  5. Exercise

Cleanup Should Belong to an Object

Memory, locks, file handles, and transactions all may need paired operations. Writing release at the end of a function misses mid-function return and exception paths. RAII lets an owning object take over a resource when it establishes a valid state, and release it in the destructor; both leaving a scope normally and exception stack unwinding complete cleanup along the same set of rules.

An RAII object itself can also be a member or a dynamic object, not limited to a local stack variable. The key is that the ultimate owner will be destroyed correctly. Standard containers manage element memory, unique_ptr manages dynamic objects, and lock_guard manages a lock’s hold period; prefer composing them rather than handwriting cleanup branches in every function.

Who Recovers Resources When Construction Fails

If a non-delegating constructor throws, the complete object’s destructor does not run, but members and bases that finished construction do clean up. If the target of a delegating constructor has already completed successfully and the delegating constructor body then throws, that object’s destructor is called. Putting resources into RAII members is therefore more reliable than raw pointer members: when a later member’s initialization fails, resources already acquired still get recovered.

Factory functions should return an owning result. The example first uses make_unique to create a Ticket, then performs a check that may fail. On exception the local unique_ptr automatically deletes the Ticket; on success the return value transfers ownership to the caller. At no moment is there a raw resource whose responsibility others must guess.

Destructors Finish Cleanup; They Do Not Report the Main Error

Destructors should try not to fail, and especially must not throw while another exception is already unwinding, or the program may terminate. If closing a file or committing a transaction must report failure to the user, provide an explicit operation; the destructor only does non-throwing fallback cleanup and does not hide a critical business result in a phase that cannot be handled.

The example uses a live-object count to observe the failure and success paths. The count is for single-threaded teaching only, not a production allocator. RAII guarantees release when objects are destroyed normally; forced process termination, power loss, and similar events do not follow ordinary stack-unwinding rules, so persistence and crash recovery still need independent design.

Pitfalls

  • Acquiring a raw resource in a constructor and then performing a throwing operation cannot rely on this class’s destructor to reclaim an object that never finished construction.
  • Calling unique_ptr::release only hands over the pointer; it does not release the resource. If no new owner takes over, RAII is broken.

Run an example

Minimum C++14 · complete program · Download .cpp

#include <cassert>
#include <memory>
#include <stdexcept>

struct Ticket {
    static int alive;
    Ticket() { ++alive; }
    ~Ticket() noexcept { --alive; }
    Ticket(const Ticket&) = delete;
    Ticket& operator=(const Ticket&) = delete;
};
int Ticket::alive = 0;

std::unique_ptr<Ticket> make_ticket(bool valid) {
    auto result = std::make_unique<Ticket>();
    if (!valid) throw std::invalid_argument("invalid ticket");
    return result;
}

int main() {
    bool caught = false;
    try { auto ticket = make_ticket(false); }
    catch (const std::invalid_argument&) { caught = true; }
    assert(caught && Ticket::alive == 0);
    {
        auto ticket = make_ticket(true);
        assert(Ticket::alive == 1);
    }
    assert(Ticket::alive == 0);
}

Compile locally

g++ -std=c++14 -Wall -Wextra -Wpedantic -pthread memory-raii.cpp -o example && ./example

Expected result

Expected: exit 0, no output; every assert holds.

CHECK YOUR UNDERSTANDING

Close the answer. Explain it.

If make_unique in make_ticket is changed to a raw new, a delete is required before the check fails to stay safe. What is a better fix?

Show a reference answer

Keep or immediately establish a unique_ptr owner so every exit path shares the same destructor cleanup. Do not add a delete branch that only covers the current exception, because later throwing operations would open holes again. On success, return the unique_ptr directly so the caller gets clear exclusive ownership.

Check the sources

Drafts and official chapters change. The version mark is only the example’s minimum.

Back to the catalog