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
- RAII: Tie resource lifetime to object lifetime
- Constructor: Acquire resources
- Destructor: Release resources
- Exception safety: Automatic cleanup even on exceptions
- Smart pointers: Memory RAII
- lock_guard: Mutex RAII
- Custom RAII: Wrap any resource (files, sockets, transactions)
RAII Benefits
| Benefit | Description |
|---|---|
| Exception safety | Cleanup guaranteed even on exceptions |
| No leaks | Resources automatically released |
| Simpler code | No manual cleanup paths |
| Composable | Multiple RAII objects work together |
| Maintainable | Less 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?
Related Articles
- 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 등으로 검색하시면 이 글이 도움이 됩니다.