Skip to content

RAII, Ownership, and Smart Pointers

RAII—resource acquisition is initialization—binds a resource to an object's lifetime. The resource may be heap memory, a file, a mutex, a socket, a transaction, or any state requiring paired cleanup.

Scope is a cleanup mechanism

cpp
#include <fstream>
#include <string>

bool write_report(const std::string& path) {
    std::ofstream output{path};
    if (!output) return false;
    output << "ready\n";
    return static_cast<bool>(output);
} // output closes here on every return path

int main() {
    return write_report("report.tmp") ? 0 : 1;
}

The stream destructor closes the file on early return and during stack unwinding if a later operation throws. Streams set state flags rather than throwing by default; callers can inspect the state, or explicitly enable exceptions. Destruction should make cleanup unavoidable, while explicit operations report failures that cannot sensibly be handled from a destructor.

unique_ptr is exclusive heap ownership

cpp
auto report = std::make_unique<Report>(configuration);
consume(std::move(report));

unique_ptr cannot be copied. Moving transfers ownership and leaves the source empty. Prefer make_unique because it constructs the object and owner together. Often no pointer is needed at all: use a direct local value or store values in a container.

Custom deleters adapt C handles:

cpp
struct FileCloser {
    void operator()(std::FILE* file) const noexcept {
        if (file) std::fclose(file);
    }
};

using File = std::unique_ptr<std::FILE, FileCloser>;

shared_ptr represents shared ownership, not general sharing

shared_ptr maintains a reference-counted control block. Copying extends lifetime; destruction decrements the strong count. It solves the specific case where no single owner can determine lifetime.

Costs include allocation/control metadata, atomic reference-count operations in common implementations, less predictable destruction, and the possibility of cycles. Two objects holding shared_ptr to one another never reach zero. Use weak_ptr for a non-owning link that can be tested and temporarily locked.

If architecture has one clear owner and many borrowers, unique_ptr plus references/pointers expresses it better.

Raw pointers and references are useful non-owners

A raw pointer need not mean dangerous ownership. It often means optional borrow; a reference means required borrow. The API should prevent borrowed handles from escaping beyond the owner's lifetime.

Views such as span, string_view, iterators, and ranges are also borrows. Container reallocation or erasure may invalidate them even while the container itself lives.

Locks are resources too

cpp
std::lock_guard lock{mutex};
update_shared_state();

The guard releases the mutex at scope exit. Manual lock()/unlock() pairs are vulnerable to early returns and exceptions. RAII converts a temporal rule into an object-lifetime rule visible to the compiler and reviewer.

Optional prompts

Explain: When is shared_ptr justified?

Answer: When lifetime is genuinely co-owned and no single owner can determine it. It should not replace a clear ownership design merely because several functions access the object.

Debug: A view into a vector dangles after push_back. What happened?

Answer: Growth may reallocate the vector's storage, invalidating pointers, references, iterators, and spans into the old allocation.

Further reference