Skip to content
C++
Language
since C++11
Beginner

Manage Resources Safely with RAII

Learn how RAII ties resource cleanup to object lifetime so you never leak memory, files, or locks again.

By the end of this page, you will understand why resource leaks happen, how C++ destructors prevent them automatically, and how to write your own RAII wrapper and use standard library tools like std::unique_ptr and std::lock_guard to manage any resource without ever writing cleanup code twice.

What and Why

Imagine you borrow a library book. Normally you return it when you are done. But what if you get distracted halfway through the day and never get around to it? That book is gone from circulation β€” leaked β€” even though it still exists somewhere.

Programs face the same problem with resources: open files, allocated memory, network connections, locked mutexes. Every resource you acquire must eventually be released. If you forget β€” or if your code exits early through a return statement or an exception β€” the resource is gone.

Many languages rely on a garbage collector to handle at least memory. C++ has no garbage collector. You are responsible.

RAII (Resource Acquisition Is Initialization) is the C++ idiom that solves this. The core idea fits in one sentence:

Tie the lifetime of a resource to the lifetime of an object β€” acquire in the constructor, release in the destructor.

C++ guarantees that an object's destructor runs the moment the object goes out of scope, even when an exception is thrown. That guarantee is the engine behind RAII.

Step by Step

The problem without RAII

Here is code that opens a file without any safety net:

cpp
#include <cstdio>

void process(bool fail_early) {
    FILE* f = std::fopen("log.txt", "w");

    if (fail_early) {
        return;               // file handle is never closed β€” leak
    }

    std::fputs("done\n", f);
    std::fclose(f);           // only reached on the happy path
}

int main() {
    process(true);   // leaks the file handle
    process(false);  // works correctly
}

Every branch that does not reach std::fclose leaks the handle. Add more branches, more early returns, or any exception, and the problem multiplies.

Wrapping the resource in a class

Move the cleanup into a destructor so it cannot be skipped:

cpp
#include <cstdio>
#include <stdexcept>

class FileHandle {
public:
    explicit FileHandle(const char* path, const char* mode)
        : file_(std::fopen(path, mode))
    {
        if (!file_) throw std::runtime_error("could not open file");
    }

    ~FileHandle() { std::fclose(file_); }

    FILE* get() const { return file_; }

    FileHandle(const FileHandle&)            = delete;
    FileHandle& operator=(const FileHandle&) = delete;

private:
    FILE* file_;
};

void process(bool fail_early) {
    FileHandle f("log.txt", "w");  // resource acquired

    if (fail_early) {
        return;   // destructor runs here β€” file is closed safely
    }

    std::fputs("done\n", f.get());
}   // destructor runs here too β€” always closed

int main() {
    process(true);
    process(false);
}

The = delete on the copy constructor and copy assignment prevents accidentally duplicating the file handle, which would cause a double-close crash. You will see this pattern in every correct RAII class that owns a unique resource.

Using the standard library

C++11 ships with ready-made RAII types for the most common resources. You rarely need to write your own:

cpp
#include <memory>
#include <iostream>

struct Connection {
    explicit Connection(int id) : id_(id) {
        std::cout << "Connection " << id_ << " opened\n";
    }
    ~Connection() {
        std::cout << "Connection " << id_ << " closed\n";
    }
    int id_;
};

int main() {
    auto conn = std::make_unique<Connection>(7);
    // conn->id_ is accessible here
}   // unique_ptr destructor deletes Connection automatically

Output:

cpp
Connection 7 opened
Connection 7 closed

No delete. No leak. Scope does the work.

Common Patterns

Pattern 1 β€” dynamic memory with std::unique_ptr

cpp
#include <memory>
#include <vector>

struct Mesh {
    std::vector<float> vertices;
    explicit Mesh(int n) : vertices(n, 0.f) {}
};

std::unique_ptr<Mesh> load_mesh(int vertex_count) {
    return std::make_unique<Mesh>(vertex_count);
}

int main() {
    auto mesh = load_mesh(1024);
    // mesh is deleted automatically when it goes out of scope
}

Pattern 2 β€” mutexes with std::lock_guard

cpp
#include <mutex>
#include <iostream>

std::mutex g_mutex;

void safe_print(const char* msg) {
    std::lock_guard<std::mutex> lock(g_mutex);  // mutex locked here
    std::cout << msg << '\n';
}   // lock_guard destructor releases the mutex here, no matter what

int main() {
    safe_print("hello from RAII");
}

Without lock_guard you would need a matching unlock() call in every exit path β€” easy to miss under error conditions.

Pattern 3 β€” file streams from the standard library

std::ifstream and std::ofstream are themselves RAII types: they open on construction and close on destruction.

cpp
#include <fstream>

int main() {
    std::ofstream out("notes.txt");  // file opened
    out << "C++ RAII is elegant\n";
}   // destructor closes and flushes the file

What Can Go Wrong

Mistake 1 β€” Thinking raw pointers manage themselves

cpp
// WRONG β€” raw pointer, no RAII
#include <string>

void bad() {
    std::string* s = new std::string("oops");
    return;   // s is never deleted β€” memory leaked
}

int main() { bad(); }
cpp
// RIGHT β€” unique_ptr owns the string
#include <memory>
#include <string>

void good() {
    auto s = std::make_unique<std::string>("great");
}   // deleted automatically

int main() { good(); }

A raw pointer is just an address. It has no destructor. Only hand raw pointers to RAII types.

Mistake 2 β€” Trying to copy a uniquely-owned resource

cpp
#include <memory>

int main() {
    auto a = std::make_unique<int>(10);
    // auto b = a;           // compile error β€” unique_ptr is not copyable
    auto b = std::move(a);  // correct: transfer ownership
    // a is now empty; b owns the int
}

std::unique_ptr is move-only. The compiler stops you from accidentally creating two owners for one allocation. If you genuinely need shared ownership, use std::shared_ptr.

Mistake 3 β€” Manually deleting a pointer already owned by a smart pointer

cpp
#include <memory>

int main() {
    int* raw = new int(5);
    std::unique_ptr<int> p(raw);

    // delete raw;   // never do this β€” p will delete it again in its destructor
    //               // double-delete is undefined behavior
}

Once a raw pointer is handed to a smart pointer, treat the raw pointer as if it does not exist.

Quick Reference

ResourceRAII typeAcquiredReleased
Heap memory (sole owner)std::unique_ptr<T>make_unique<T>(...)destructor calls delete
Heap memory (shared)std::shared_ptr<T>make_shared<T>(...)last destructor calls delete
Mutexstd::lock_guard<M>constructor locksdestructor unlocks
File streamstd::ifstream / std::ofstreamconstructor opensdestructor closes
Any other resourcewrite your own classconstructor acquiresdestructor releases

Rules to keep close:

  • Acquire in the constructor, release in the destructor.
  • Delete the copy constructor and copy assignment in any class that uniquely owns a resource.
  • Prefer std::make_unique over new. Prefer std::make_shared over new.
  • Never manually delete a pointer that a smart pointer already owns.

What's Next