본문으로 건너뛰기
Previous
Next
C++ RAII Pattern Complete Guide

C++ RAII Pattern Complete Guide

C++ RAII Pattern Complete Guide

이 글의 핵심

Master C++ RAII: automatic resource cleanup through constructor/destructor pairing. Complete guide with smart pointers, locks, and production patterns.

What is RAII?

RAII ties object lifetime and resource management into broader C++ design pattern material in overview #20-2 and RAII deep dive. Resource Acquisition Is Initialization

  • Resource acquisition is initialization
  • Acquire resources in constructor
  • Release resources in destructor
// ❌ Manual management (dangerous)
void badExample() {
    int* ptr = new int(10);
    // ....complex logic ...
    if (error) return;  // Memory leak!
    delete ptr;
}
// ✅ RAII (safe)
void goodExample() {
    unique_ptr<int> ptr = make_unique<int>(10);
    // ....complex logic ...
    if (error) return;  // Automatically deleted!
}  // Automatically deleted!

Key benefit: Resources are automatically released when object goes out of scope, even on exceptions or early returns.

Basic RAII Class

class FileHandler {
private:
    FILE* file;
    
public:
    // Constructor: acquire resource
    FileHandler(const char* filename) {
        file = fopen(filename, "w");
        if (!file) {
            throw runtime_error("Failed to open file");
        }
        cout << "File opened" << endl;
    }
    
    // Destructor: release resource
    ~FileHandler() {
        if (file) {
            fclose(file);
            cout << "File closed" << endl;
        }
    }
    
    // Prohibit copy
    FileHandler(const FileHandler&) = delete;
    FileHandler& operator=(const FileHandler&) = delete;
    
    void write(const char* data) {
        fprintf(file, "%s\n", data);
    }
};
int main() {
    try {
        FileHandler fh("output.txt");
        fh.write("Hello");
        fh.write("World");
    } catch (exception& e) {
        cerr << e.what() << endl;
    }
    // File automatically closed
}

Output:

File opened
File closed

Explanation: Even if exception occurs or early return happens, destructor is called, ensuring file is closed.

Smart Pointers (RAII Examples)

unique_ptr

#include <memory>
class Resource {
public:
    Resource() { cout << "Resource created" << endl; }
    ~Resource() { cout << "Resource released" << endl; }
    void use() { cout << "Resource used" << endl; }
};
void process() {
    auto res = make_unique<Resource>();
    res->use();
    
    if (someCondition) {
        return;  // Automatic release
    }
    
    // Even if exception occurs, automatic release
    throw runtime_error("error");
}  // Automatic release

Output:

Resource created
Resource used
Resource released

shared_ptr

class Data {
public:
    Data() { cout << "Data created" << endl; }
    ~Data() { cout << "Data released" << endl; }
};
void shareData() {
    auto data = make_shared<Data>();
    
    {
        auto data2 = data;  // Reference count increases
        cout << "Ref count: " << data.use_count() << endl;  // 2
    }  // data2 destroyed, ref count decreases
    
    cout << "Ref count: " << data.use_count() << endl;  // 1
}  // data destroyed, Data released

Output:

Data created
Ref count: 2
Ref count: 1
Data released

lock_guard (Mutex RAII)

#include <mutex>
#include <thread>
mutex mtx;
int counter = 0;
void increment() {
    // ❌ Manual lock/unlock
    mtx.lock();
    counter++;
    if (error) {
        // mtx.unlock();  // Forget this → deadlock!
        return;
    }
    mtx.unlock();
}
void incrementSafe() {
    // ✅ RAII
    lock_guard<mutex> lock(mtx);  // Automatic lock
    counter++;
    if (error) {
        return;  // Automatic unlock
    }
}  // Automatic unlock

Key: lock_guard automatically unlocks mutex on scope exit, preventing deadlocks from forgotten unlocks.

Practical Examples

Example 1: Database Connection

class DatabaseConnection {
private:
    void* connection;
    
public:
    DatabaseConnection(const string& connectionString) {
        connection = openConnection(connectionString);
        if (!connection) {
            throw runtime_error("DB connection failed");
        }
        cout << "DB connected" << endl;
    }
    
    ~DatabaseConnection() {
        if (connection) {
            closeConnection(connection);
            cout << "DB connection closed" << endl;
        }
    }
    
    void execute(const string& query) {
        // Execute query
    }
};
void processData() {
    DatabaseConnection db("localhost:5432");
    db.execute("SELECT * FROM users");
    // Even if exception occurs, connection automatically closed
}

Output:

DB connected
DB connection closed

Example 2: Timer

#include <chrono>
class Timer {
private:
    chrono::time_point<chrono::high_resolution_clock> start;
    string name;
    
public:
    Timer(const string& n) : name(n) {
        start = chrono::high_resolution_clock::now();
        cout << name << " started" << endl;
    }
    
    ~Timer() {
        auto end = chrono::high_resolution_clock::now();
        auto duration = chrono::duration_cast<chrono::milliseconds>(end - start);
        cout << name << " completed: " << duration.count() << "ms" << endl;
    }
};
void expensiveOperation() {
    Timer timer("expensiveOperation");
    
    // Complex work...
    this_thread::sleep_for(chrono::seconds(1));
    
}  // Automatically measures time

Output:

expensiveOperation started
expensiveOperation completed: 1000ms

Example 3: State Restoration

class StateGuard {
private:
    int& state;
    int oldState;
    
public:
    StateGuard(int& s, int newState) : state(s), oldState(s) {
        state = newState;
        cout << "State changed: " << oldState << " -> " << newState << endl;
    }
    
    ~StateGuard() {
        state = oldState;
        cout << "State restored: " << oldState << endl;
    }
};
void temporaryStateChange() {
    int globalState = 0;
    
    {
        StateGuard guard(globalState, 1);
        // globalState is 1
        cout << "Current state: " << globalState << endl;
    }  // Automatically restored to 0
    
    cout << "Restored state: " << globalState << endl;
}

Output:

State changed: 0 -> 1
Current state: 1
State restored: 0
Restored state: 0

Example 4: Scope Guard

template<typename F>
class ScopeGuard {
private:
    F func;
    bool active;
    
public:
    ScopeGuard(F f) : func(f), active(true) {}
    
    ~ScopeGuard() {
        if (active) {
            func();
        }
    }
    
    void dismiss() {
        active = false;
    }
};
template<typename F>
ScopeGuard<F> makeScopeGuard(F f) {
    return ScopeGuard<F>(f);
}
void processFile() {
    FILE* file = fopen("data.txt", "r");
    auto guard = makeScopeGuard([file]() {
        if (file) {
            fclose(file);
            cout << "File closed" << endl;
        }
    });
    
    // File processing...
    if (error) {
        return;  // File automatically closed
    }
    
    guard.dismiss();  // Success: cancel automatic close
    // Manual handling...
}

Explanation: Scope guard executes cleanup function on scope exit. dismiss() cancels cleanup on success.

RAII Rules

1. Acquire Resources in Constructor

class Resource {
public:
    Resource() {
        // Acquire resource
        data = new int[100];
    }
};

2. Release Resources in Destructor

class Resource {
public:
    ~Resource() {
        // Release resource
        delete[] data;
    }
};

3. Prohibit Copy or Implement Properly

class Resource {
public:
    // Prohibit copy
    Resource(const Resource&) = delete;
    Resource& operator=(const Resource&) = delete;
    
    // Or allow move only
    Resource(Resource&& other) noexcept {
        data = other.data;
        other.data = nullptr;
    }
};

Common Issues

Issue 1: Exception in Constructor

// ❌ Dangerous
class Bad {
private:
    int* data1;
    int* data2;
    
public:
    Bad() {
        data1 = new int[100];
        // If exception here?
        data2 = new int[100];  // data1 leaks!
    }
};
// ✅ Safe
class Good {
private:
    unique_ptr<int[]> data1;
    unique_ptr<int[]> data2;
    
public:
    Good() {
        data1 = make_unique<int[]>(100);
        data2 = make_unique<int[]>(100);  // Even if exception, data1 auto-released
    }
};

Key: Use smart pointers for member variables to ensure cleanup even if constructor throws.

Issue 2: Exception in Destructor

// ❌ Dangerous
class Bad {
public:
    ~Bad() {
        throw runtime_error("error");  // Never do this!
    }
};
// ✅ Safe
class Good {
public:
    ~Good() noexcept {
        try {
            // Risky operation
        } catch (...) {
            // Swallow exception
        }
    }
};

Key: Never throw from destructors. Swallow exceptions or log them, but don’t propagate.

Issue 3: Resource Duplication on Copy

// ❌ Shallow copy
class Bad {
private:
    int* data;
    
public:
    Bad() : data(new int(10)) {}
    ~Bad() { delete data; }
    // No copy constructor → double delete!
};
// ✅ Prohibit copy
class Good {
private:
    int* data;
    
public:
    Good() : data(new int(10)) {}
    ~Good() { delete data; }
    
    Good(const Good&) = delete;
    Good& operator=(const Good&) = delete;
};

FAQ

Q1: Why is RAII important?

A:

  • Exception safety
  • Prevents memory leaks
  • Simplifies code
  • Automatic resource management

Q2: Should I use RAII for all resources?

A: Yes, use RAII for all resources (memory, files, sockets, mutexes, etc.) whenever possible.

Q3: RAII vs try-finally?

A: C++ has no finally blocks. RAII is safer and more concise. Destructors are called during stack unwinding, guaranteeing cleanup.

Q4: Performance overhead?

A: Almost none. Compilers optimize RAII. Benefits in safety and maintainability far outweigh any cost.

Q5: When to create RAII wrappers?

A:

  • Resources requiring manual release
  • When exception safety is critical
  • When state restoration is needed

Q6: Learning resources for RAII?

A:

  • “Effective C++” (Scott Meyers)
  • “C++ Coding Standards” (Sutter & Alexandrescu)
  • STL smart pointer source code

Production Patterns

Pattern 1: Network Socket RAII

#include <sys/socket.h>
#include <unistd.h>
class Socket {
    int fd_;
public:
    Socket(const char* host, int port) {
        fd_ = socket(AF_INET, SOCK_STREAM, 0);
        if (fd_ < 0) throw std::runtime_error("Socket creation failed");
        // Connect...
    }
    ~Socket() {
        if (fd_ >= 0) close(fd_);
    }
    Socket(const Socket&) = delete;
    Socket& operator=(const Socket&) = delete;
    int fd() const { return fd_; }
};

Pattern 2: Transaction Guard

class Transaction {
    Database& db_;
    bool committed_ = false;
public:
    Transaction(Database& db) : db_(db) {
        db_.begin();
    }
    ~Transaction() {
        if (!committed_) db_.rollback();
    }
    void commit() {
        db_.commit();
        committed_ = true;
    }
};
void updateRecords(Database& db) {
    Transaction txn(db);
    db.execute("UPDATE ...");
    db.execute("INSERT ...");
    txn.commit();  // Success: commit
}  // If exception before commit, automatic rollback

Pattern 3: Temporary Directory

#include <filesystem>
namespace fs = std::filesystem;
class TempDirectory {
    fs::path path_;
public:
    TempDirectory() {
        path_ = fs::temp_directory_path() / ("tmp_" + std::to_string(std::rand()));
        fs::create_directories(path_);
    }
    ~TempDirectory() {
        std::error_code ec;
        fs::remove_all(path_, ec);
    }
    const fs::path& path() const { return path_; }
};
void processFiles() {
    TempDirectory tmpDir;
    // Use tmpDir.path() for temp files
    // Automatically cleaned up on scope exit
}

Pattern 4: OpenGL Context Guard

class GLContextGuard {
public:
    GLContextGuard() {
        glPushAttrib(GL_ALL_ATTRIB_BITS);
    }
    ~GLContextGuard() {
        glPopAttrib();
    }
};
void renderWithState() {
    GLContextGuard guard;
    glEnable(GL_BLEND);
    // ....rendering ...
}  // State automatically restored

Best Practices

1. Always Use Smart Pointers for Heap Memory

// ❌ Raw pointers
void bad() {
    Widget* w = new Widget();
    // ....may throw ...
    delete w;  // May not execute
}
// ✅ Smart pointers
void good() {
    auto w = make_unique<Widget>();
    // ....may throw ...
}  // Automatic cleanup

2. Wrap C Resources in RAII

// ✅ Wrap FILE*
class File {
    FILE* f_;
public:
    explicit File(const char* path, const char* mode) 
        : f_(std::fopen(path, mode)) {
        if (!f_) throw std::runtime_error("open failed");
    }
    ~File() { if (f_) std::fclose(f_); }
    FILE* get() { return f_; }
};

3. Use lock_guard for Mutexes

// ✅ Always use lock_guard
void criticalSection() {
    std::lock_guard<std::mutex> lock(mtx);
    // Critical section
}  // Automatic unlock

4. Prohibit Copy for Resource-Owning Classes

class Resource {
public:
    Resource(const Resource&) = delete;
    Resource& operator=(const Resource&) = delete;
    Resource(Resource&&) = default;
    Resource& operator=(Resource&&) = default;
};

5. Use Scope Guards for Cleanup Actions

auto cleanup = makeScopeGuard([]() {
    // Cleanup code
});
// Do work...
cleanup.dismiss();  // Success: cancel cleanup

Summary

Key Points

  1. RAII: Tie resource lifetime to object lifetime
  2. Constructor: Acquire resources
  3. Destructor: Release resources
  4. Exception safety: Automatic cleanup even on exceptions
  5. Smart pointers: Memory RAII
  6. lock_guard: Mutex RAII
  7. Custom RAII: Wrap any resource (files, sockets, transactions)

RAII Benefits

BenefitDescription
Exception safetyCleanup guaranteed even on exceptions
No leaksResources automatically released
Simpler codeNo manual cleanup paths
ComposableMultiple RAII objects work together
MaintainableLess error-prone than manual management

RAII Checklist

  • Resources acquired in constructor?
  • Resources released in destructor?
  • Copy prohibited or properly implemented?
  • Destructor marked noexcept?
  • Using smart pointers for heap memory?
  • Using lock_guard for mutexes?

  • C++ RAII & Smart Pointers Guide
  • C++ RAII Complete Guide | “Too many open files” Failure and Automatic Resource Management
  • C++ RAII | “Cannot open file” Failure and Automatic Resource Management

Keywords

C++ RAII, resource management, smart pointers, exception safety, lock guard, scope guard, automatic cleanup One-line summary: RAII ties resource lifetime to object lifetime, ensuring automatic cleanup through destructors even on exceptions. Essential for exception-safe C++ code.


같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.

  • C++ 디자인 패턴 종합 가이드 | Singleton·Factory
  • C++ 디자인 패턴 | Singleton·Factory·Builder·Prototype 생성 패턴 가이드
  • C++ RAII & Smart Pointers | ‘스마트 포인터’ 가이드
  • C++ RAII | ‘파일을 열 수 없습니다’ 장애의 원인과 자동 리소스 관리
  • C++ RAII 완벽 가이드 | ‘Too many open files’ 장애 원인과 리소스 자동 관리

이 글에서 다루는 키워드 (관련 검색어)

C++, RAII, resource management, smart pointers, exception safety, design patterns 등으로 검색하시면 이 글이 도움이 됩니다.