RAII: बुनियादी रिसोर्स पैटर्न
RAII (Resource Acquisition Is Initialization) सीखें — एक्सेप्शन-सेफ़ रिसोर्स मैनेजमेंट का मूल C++ पैटर्न। RAII रैपर लागू करें, lock_guard और scoped_lock को समझें, और जानें कि RAII टाइप मूव-ओनली या रेफ़रेंस-काउंटेड क्यों होने चाहिए।
RAII: कंस्ट्रक्टर में हासिल करें, डिस्ट्रक्टर में छोड़ें
RAII (Resource Acquisition Is Initialization) C++ का सबसे महत्वपूर्ण पैटर्न है। विचार सरल है:
1. कंस्ट्रक्टर में रिसोर्स हासिल करें (acquire) (फ़ाइल खोलना, म्यूटेक्स लॉक करना, मेमोरी एलोकेट करना, डेटाबेस से कनेक्ट करना)।
2. डिस्ट्रक्टर में रिसोर्स छोड़ें (release) (फ़ाइल बंद करना, म्यूटेक्स अनलॉक करना, मेमोरी मुक्त करना, डिस्कनेक्ट करना)।
क्योंकि C++ गारंटी देता है कि ऑब्जेक्ट के स्कोप से बाहर जाने पर डिस्ट्रक्टर कॉल होते हैं — भले ही एक्सेप्शन फेंका गया हो — इसलिए RAII रिसोर्स मैनेजमेंट को स्वचालित और एक्सेप्शन-सेफ़ बना देता है। आप छोड़ना कभी नहीं भूलते, और कभी दो बार नहीं छोड़ते।
रिसोर्स मैनेज करने वाला हर अच्छी तरह डिज़ाइन किया गया C++ टाइप RAII का उपयोग करता है: std::string, std::vector, std::unique_ptr, std::fstream, std::lock_guard, std::unique_lock।
RAII रैपर लागू करना
यह फ़ाइल डिस्क्रिप्टर (POSIX) के लिए एक पूरा RAII रैपर है। यह कंस्ट्रक्टर में रिसोर्स हासिल करता है, डिस्ट्रक्टर में छोड़ता है, और ओनरशिप ट्रांसफ़र के लिए मूव सिमेंटिक्स को सपोर्ट करता है।
#include <unistd.h> // open, close, read, write
#include <fcntl.h> // O_RDONLY, etc.
#include <stdexcept>
#include <utility>
#include <iostream>
class FileDescriptor {
int fd_ = -1; // -1 = no resource
public:
// Acquire: open file in constructor
explicit FileDescriptor(const char* path, int flags)
: fd_(::open(path, flags)) {
if (fd_ < 0) throw std::runtime_error("Cannot open file");
}
// Release: close file in destructor
~FileDescriptor() {
if (fd_ >= 0) ::close(fd_);
}
// Non-copyable (copying a file descriptor is meaningless)
FileDescriptor(const FileDescriptor&) = delete;
FileDescriptor& operator=(const FileDescriptor&) = delete;
// Moveable (transfer ownership)
FileDescriptor(FileDescriptor&& other) noexcept
: fd_(std::exchange(other.fd_, -1)) {}
FileDescriptor& operator=(FileDescriptor&& other) noexcept {
if (this != &other) {
if (fd_ >= 0) ::close(fd_);
fd_ = std::exchange(other.fd_, -1);
}
return *this;
}
int get() const { return fd_; }
};
int main() {
try {
FileDescriptor fd("/tmp/raii_test.txt", O_RDONLY);
std::cout << "File opened: fd=" << fd.get() << '\n';
// Use fd...
} catch (const std::exception& e) {
std::cerr << e.what() << '\n';
}
// fd.~FileDescriptor() called automatically — file closed
// Even if an exception was thrown, the file is closed
}RAII के ज़रिए एक्सेप्शन सेफ़्टी
RAII के बिना, एक्सेप्शन-सेफ़ कोड के लिए हर रिसोर्स पर उबाऊ try/catch ब्लॉक चाहिए। RAII के साथ यह स्वचालित है — स्टैक अनवाइंडिंग के दौरान हर डिस्ट्रक्टर अपने-आप चल जाता है।
#include <memory>
#include <mutex>
#include <fstream>
#include <vector>
#include <stdexcept>
std::mutex g_mutex;
// WITHOUT RAII — manual cleanup is error-prone
void bad_example() {
std::mutex* m = &g_mutex;
m->lock();
int* data = new int[100];
// If this throws, mutex stays locked AND data leaks!
// process(data);
delete[] data;
m->unlock();
}
// WITH RAII — automatic, exception-safe cleanup
void good_example() {
std::lock_guard<std::mutex> lock(g_mutex); // RAII lock
auto data = std::make_unique<int[]>(100); // RAII allocation
// If this throws:
// 1. data's destructor frees the memory
// 2. lock's destructor unlocks the mutex
// Stack unwinding handles everything
// process(data.get());
}
// Multiple resources — RAII composes naturally
void multi_resource() {
std::lock_guard<std::mutex> lock(g_mutex);
auto buffer = std::make_unique<char[]>(4096);
std::ofstream file("/tmp/output.txt");
// If any operation throws, ALL resources are released
// in reverse order of construction:
// 1. file closed
// 2. buffer freed
// 3. mutex unlocked
}lock_guard, unique_lock और scoped_lock
स्टैंडर्ड लाइब्रेरी म्यूटेक्स के लिए RAII रैपर देती है:
- std::lock_guard — सबसे सरल RAII लॉक। कंस्ट्रक्टर में लॉक करता है, डिस्ट्रक्टर में अनलॉक करता है। न मूव हो सकता है, न कॉपी। जब आपको सिर्फ़ एक सरल स्कोप वाला लॉक चाहिए, तब इसका उपयोग करें।
- std::unique_lock — लचीला RAII लॉक। यह डिफ़र्ड लॉकिंग, टाइम्ड लॉकिंग, मैन्युअल अनलॉक/रीलॉक सपोर्ट करता है, और मूव किया जा सकता है। std::condition_variable के साथ उपयोग के लिए ज़रूरी है।
- std::scoped_lock (C++17) — डेडलॉक से बचाने वाले एल्गोरिदम के ज़रिए एक साथ कई म्यूटेक्स लॉक करता है। एक या अधिक म्यूटेक्स लॉक करते समय यही पसंदीदा लॉक टाइप है।
std::mutex m1, m2;
// C++17: बिना डेडलॉक के जोखिम के दोनों को लॉक करें
std::scoped_lock lock(m1, m2);1. बिना नाम वाले RAII ऑब्जेक्ट तुरंत नष्ट हो जाते हैं:
std::lock_guard<std::mutex>(mtx); // बग! नाम के बिना बना अस्थायी ऑब्जेक्ट लॉक करके तुरंत अनलॉक कर देता है। (नोट: असल C++ में यह पंक्ति mtx की डिक्लेरेशन के रूप में पार्स होती है और कंपाइल नहीं होती; अस्थायी ऑब्जेक्ट तब बनता है जब ब्रेस-इनिशियलाइज़ेशन जैसा रूप इस्तेमाल हो)
std::lock_guard<std::mutex> lock(mtx); // सही — नाम वाला, स्कोप के अंत तक जीवित रहता है2. RAII टाइप को बिना सोचे-समझे कॉपी न करें: फ़ाइल हैंडल या म्यूटेक्स लॉक को कॉपी करने का कोई मतलब नहीं है। RAII टाइप ये होने चाहिए:
- विशिष्ट ओनरशिप के लिए मूव-ओनली (जैसे unique_ptr, unique_lock), या
- साझा ओनरशिप के लिए रेफ़रेंस-काउंटेड (जैसे shared_ptr)।
3. डिस्ट्रक्टर को एक्सेप्शन नहीं फेंकना चाहिए: अगर स्टैक अनवाइंडिंग के दौरान (जब कोई दूसरा एक्सेप्शन सक्रिय हो) कोई डिस्ट्रक्टर एक्सेप्शन फेंके, तो std::terminate कॉल हो जाता है। C++11 और उसके बाद डिस्ट्रक्टर डिफ़ॉल्ट रूप से noexcept होते हैं।
1. हर रिसोर्स का मालिक एक RAII ऑब्जेक्ट होना चाहिए — मेमोरी, फ़ाइलें, लॉक, सॉकेट, डेटाबेस कनेक्शन, GPU हैंडल।
2. हर RAII क्लास में एक रिसोर्स — रैपर को केंद्रित रखें।
3. कॉपी ऑपरेशन को delete करें, जब तक कि रिसोर्स को कॉपी करना अर्थपूर्ण न हो।
4. ओनरशिप ट्रांसफ़र संभव बनाने के लिए मूव ऑपरेशन लागू करें।
5. मूव ऑपरेशन में std::exchange का उपयोग करें — यह सोर्स का पुराना मान पढ़कर उसी एक्सप्रेशन में नया मान सेट कर देता है (यह थ्रेड-सेफ़ अर्थ में एटॉमिक नहीं है): fd_ = std::exchange(other.fd_, -1);
6. मानक RAII टाइप को प्राथमिकता दें (unique_ptr, lock_guard, fstream) — कस्टम रैपर केवल उन रिसोर्स के लिए लिखें जिनके मानक रैपर नहीं हैं।
RAII टाइप को जोड़ना (रूल ऑफ़ ज़ीरो)
RAII की चरम अभिव्यक्ति रूल ऑफ़ ज़ीरो है: अगर आपके सभी मेंबर वेरिएबल RAII टाइप हैं, तो आपकी क्लास को किसी यूज़र-डिक्लेयर्ड स्पेशल मेंबर की ज़रूरत नहीं है। कंपाइलर द्वारा जनरेट किए गए डिस्ट्रक्टर, कॉपी और मूव ऑपरेशन अपने-आप सही काम करते हैं।
class DatabaseSession {
std::unique_ptr<Connection> conn_; // RAII: अपने-आप बंद होता है
std::unique_lock<std::mutex> lock_; // RAII: अपने-आप अनलॉक होता है
std::vector<std::string> log_; // RAII: अपने-आप मुक्त होता है
// डिस्ट्रक्टर, कॉपी या मूव की ज़रूरत नहीं — सभी मेंबर टाइप यह संभाल लेते हैं
};रूल ऑफ़ ज़ीरो ही लक्ष्य है। रूल ऑफ़ फ़ाइव (सभी 5 स्पेशल मेंबर स्पष्ट रूप से परिभाषित करना) निम्न-स्तरीय रिसोर्स रैपर टाइप के लिए है। इसमें कोई बीच का रास्ता नहीं है — अगर आप पाँच में से कोई भी डिक्लेयर करते हैं, तो सभी डिक्लेयर करें।
- RAII: कंस्ट्रक्टर में हासिल करें, डिस्ट्रक्टर में छोड़ें — स्वचालित और एक्सेप्शन-सेफ़
- स्टैक अनवाइंडिंग के दौरान डिस्ट्रक्टर कॉल होते हैं, इसलिए RAII एक्सेप्शन को स्वाभाविक रूप से संभालता है
- RAII टाइप मूव-ओनली (विशिष्ट ओनरशिप) या रेफ़रेंस-काउंटेड (साझा ओनरशिप) होने चाहिए
- डेडलॉक-मुक्त मल्टी-म्यूटेक्स लॉकिंग के लिए
std::scoped_lock(C++17) का उपयोग करें - बिना नाम वाले RAII ऑब्जेक्ट तुरंत नष्ट हो जाते हैं — उन्हें हमेशा नाम दें
- रूल ऑफ़ ज़ीरो का पालन करें: RAII मेंबर इस्तेमाल करें ताकि कंपाइलर सही स्पेशल मेंबर जनरेट करे
क्विज़ — अपना ज्ञान परखें
(15 XP)1. मान लें कि बिना नाम का अस्थायी lock_guard बनाया जाता है, जैसा `std::lock_guard<std::mutex>(mtx);` से इरादा था (ध्यान दें: असल C++ में यह पंक्ति mtx नाम के वेरिएबल की डिक्लेरेशन के रूप में पार्स होती है और कंपाइल नहीं होती; अस्थायी ऑब्जेक्ट तब बनता है जब ब्रेस-इनिशियलाइज़ेशन जैसा रूप इस्तेमाल हो)। ऐसे बिना नाम के अस्थायी lock_guard में क्या गलत है?
2. RAII टाइप को आमतौर पर अपने कॉपी ऑपरेशन delete क्यों करने चाहिए?
3. रूल ऑफ़ ज़ीरो क्या है?