5.2.13 · D3 · Coding › C++ Programming › RAII — resource acquisition is initialization — why it's the
Yeh page RAII idiom ko har tarah ki situation mein walk karta hai jisme use survive karna hota hai. Parent note ne bataya tha ki RAII kya hai; yahan hum usse prove karte hain — har corner case ko hit karke — happy path, exception path, double-owner trap, empty resource, nested scope, aur ek exam-style twist.
Har example se pehle, forecast karo : khud guess karo answer. Phir check karo.
Har "resource lifetime" problem inhi cells mein se kisi ek mein aata hai. Hum har ek ko hit karenge.
Cell
Kya trigger karta hai
Danger agar RAII absent ho
Example
A. Normal exit
function cleanly return karta hai
kuch nahi (lekin hand-cleanup verbose hota hai)
Ex 1
B. Exception exit
koi callee mid-function throw karta hai
resource leak / locked rehta hai
Ex 2
C. Early return / break
cleanup se pehle if(...) return;
release skip ho jaata hai
Ex 3
D. Copy of an owner
ek owner ko doosre mein assign/copy kiya
double-free
Ex 4
E. Move (steal)
ownership transfer ho jaati hai
double-free YA no free
Ex 5
F. Empty / null resource
acquisition fail hua ya moved-from hai
destructor no-op hona chahiye
Ex 6
G. Nested / reverse order
ek scope mein kai owners
galat teardown order → use-after-free
Ex 7
H. Real-world word problem
file + lock saath mein
leak + deadlock combined
Ex 8
I. Exam twist (throwing dtor)
destructor throws while unwinding
std::terminate
Ex 9
Har cell letter apne example pe ek tag ki tarah lagta hai. Saath mein yeh poora space tile kar dete hain un cheezoon ka jo galat ho sakti hain jab ek resource acquire hota hai.
Jinpe hum rely karte hain: Constructors and Destructors , Stack Unwinding and Exceptions , Smart Pointers - unique_ptr shared_ptr , Rule of Three Five Zero , Move Semantics , Exception Safety Guarantees , aur std::lock_guard and Mutexes .
Cleanup ko numbers se checkable banane ke liye, socho ki har resource ek global ledger maintain karta hai:
Definition Acquire/release ledger
Jab bhi koi resource acquire hota hai, hum live counter mein +1 add karte hain. Jab bhi woh release hota hai, hum 1 subtract karte hain. Ek program leak-free aur double-free-free hai exactly jab, end mein, ==live == 0 ho aur yeh kabhi negative na gaya ho== us poore time mein. Negative jaana matlab hoga "kuch aisa release kiya jo hum own nahi karte the" (double-free); positive end hona matlab "kabhi release hi nahi kiya" (leak).
Figure dekho: black line live ko time ke saath trace karti hai. RAII use 0 tak ek clean staircase mein wapas laata hai.
Worked example Example 1 — Cell A: normal exit
void f () {
std ::vector <int> v ( 100 ); // acquire: live 0 -> 1
use (v);
} // dtor: live 1 -> 0
Forecast: function end par, live kya hai? Kitne deletes tumne likhe?
Constructor runs. v(100) ek buffer allocate karta hai. Yeh step kyun? Acquisition is initialization — memory constructor ke andar grab hoti hai, live 1 ho jaata hai.
use(v) normally return karta hai. Yeh step kyun? Yeh boring happy path hai; kuch bhi flow interrupt nahi karta.
Scope ends, destructor runs. Yeh step kyun? Block chhorna locals ko destroy karta hai; ~vector buffer free karta hai, live 0 ho jaata hai.
Verify: live 0 se start, +1, -1 → 0 pe khatam. Hand-written deletes = 0. ✅
Worked example Example 2 — Cell B: exception exit
void f () {
auto p = std :: make_unique < int >( 42 ); // acquire: live 0 -> 1
might_throw (); // throws!
// ...kabhi reach nahi hoga...
} // dtor STILL runs: live 1 -> 0
Forecast: throw f se bahar propagate hone ke baad, kya int leak hua?
Constructor mein acquire. make_unique allocate karta hai, live = 1. Yeh step kyun? Ownership ab p ke andar hai, jo ek stack object hai.
might_throw() throw karta hai. Yeh step kyun? Yahi woh killer hai jiske baare mein parent ne warn kiya tha — is line ke baad ek hand-written delete skip ho jaata.
Stack unwinding fire hoti hai. Jaise exception f se bahar jaata hai, har local destroy hota hai. ~unique_ptr delete call karta hai, live = 0. Yeh step kyun? Stack unwinding woh language guarantee hai jis par RAII sawaar hota hai — kisi bhi exit par cleanup.
Verify: ledger 0 → +1 → -1 = 0, aur kabhi negative nahi gaya. Koi leak nahi even though acquisition ke baad wala code kabhi nahi chala. ✅
Worked example Example 3 — Cell C: early return / break
int f ( bool skip ) {
std ::lock_guard < std ::mutex > g (m); // acquire: locks 0 -> 1
if (skip) return 1 ; // early return
do_work ();
return 0 ;
} // dtor unlocks: locks 1 -> 0
Forecast: agar skip == true ho, toh kya mutex locked rehta hai?
Constructor mein lock acquire karo. locks = 1. Yeh step kyun? lock_guard construction par lock karta hai.
return 1 early hit karo. Yeh step kyun? Neeche likha manual unlock() jump over ho jaata — yeh classic multi-exit bug hai.
g ka destructor har return par runs. return 1 aur return 0 dono scope chhod dete hain, toh ~lock_guard unlock karta hai, locks = 0. Yeh step kyun? Scope-bound cleanup har exit path par fire hoti hai, sirf last line par nahi.
Verify: dono ke liye skip = true aur skip = false: locks 0 par khatam hota hai. Koi bhi branch lock leak nahi karta. ✅
Worked example Example 4 — Cell D: owner ko copy karna (double-free trap)
struct BadOwner {
int* p;
BadOwner () : p ( new int ( 7 )) {} // live 0 -> 1
~BadOwner () { delete p; } // p ko free karta hai
// no copy control -> default copies the POINTER value
};
void f () {
BadOwner a; // live 1
BadOwner b = a; // b.p == a.p (same pointer!)
} // ~b frees, ~a frees SAME address -> double free
Forecast: same address kitni baar free hota hai?
a construct karo. live = 1, ek allocation. Yeh step kyun? Resource establish hota hai.
a ko b mein copy karo compiler ke default copy se. Yeh step kyun? Default sirf raw p value copy karta hai — ab do objects sochte hain ki woh ek buffer own karte hain.
Dono destructors run karte hain (reverse order: pehle b phir a). Har ek same address par delete p call karta hai → ledger 1 → 0 → -1 ho jaata hai. Yeh step kyun? Negative jaana double-free ka numeric signature hai.
Fix (Rule of Five): copy operations delete karo, ya std::unique_ptr<int> use karo (jo non-copyable hai), ya deep-copy karo. unique_ptr ke saath b = a; line compile hi nahi karegi , bug ko build time par pakad legi. Dekho Rule of Three Five Zero .
Verify: naive ledger -1 tak pahunchta hai (double free); fixed version 1 → 0 par rehta hai. ✅
Worked example Example 5 — Cell E: move = steal and null
auto a = std :: make_unique < int >( 9 ); // a owns buffer, live 1
auto b = std :: move (a); // steal: b owns it, a becomes null
// ...end of scope... // ~b frees (live -> 0), ~a is a no-op
Forecast: move ke baad, buffer ke kitne owners hain? a ab kya hai?
a acquire karta hai. live = 1. Yeh step kyun? Ek owner exist karta hai.
std::move(a) ownership b ko transfer karta hai. b pointer leta hai; a ka pointer nullptr set ho jaata hai . Yeh step kyun? Move cheaply ownership hand over karta hai — exactly ek owner har time, toh no double-free (Ex 4 se contrast karo).
Dono destroy karte hain. ~b ek baar free karta hai (live = 0); ~a nullptr dekhta hai aur kuch nahi karta. Yeh step kyun? Steal-then-null invariant ka matlab hai ki emptied source safely destroy ho sakta hai.
Verify: ledger 0 → +1 → -1 = 0, exactly ek baar free hua; a nullptr hold karta hai. ✅
Worked example Example 6 — Cell F: empty / null resource (degenerate input)
std ::unique_ptr <int> p; // nullptr hold karta hai, KABHI acquire nahi hua: live 0 rehta hai
// ...
// p out of scope jaata hai: destructor nullptr dekhta hai, nullptr par delete call karta hai (safe no-op)
Forecast: ek default-constructed unique_ptr kuch bhi own nahi karta — kya uska destructor crash karta hai?
Default construct. p nullptr hold karta hai; koi acquisition nahi , live = 0. Yeh step kyun? Yeh degenerate case hai: ek owner jo kuch own nahi karta (yahi Ex 5 ke moved-from object ki state bhi hai).
Destructor runs. Woh delete nullptr karta hai, jise C++ standard no-op define karta hai. Yeh step kyun? Ek correct RAII destructor ko empty case guard karna chahiye (if (f) parent ke FileHandle mein) taaki woh harmless rahe.
Verify: ledger kabhi move nahi karta — 0 se start, 0 par end. Empty par koi crash nahi. ✅
Worked example Example 7 — Cell G: nested scope, reverse-order teardown
struct Logger { const char* name; int order;
Logger ( const char* n , int o ): name (n), order (o) {} // acquire "log slot"
~Logger () { /* teardown record karta hai */ } };
void g () {
Logger a ( "a" , 1 ); // pehle construct hua
Logger b ( "b" , 2 ); // doosre construct hua (a use kar sakta hai)
} // pehle b (2) destroy hua PHIR a (1) -> LIFO
Forecast: destructors kis order mein run karte hain — pehle a phir b, ya pehle b phir a?
Pehle a construct karo, phir b. Construction order 1, 2. Yeh step kyun? Baad ke objects pehle wale par depend kar sakte hain (b koi resource use kar sakta hai jo a own karta hai).
Scope khatam → reverse mein destroy karo (LIFO): pehle b (order 2), phir a (order 1). Yeh step kyun? Reverse order har dependency ko valid rakhta hai jab tak dependent gone na ho jaaye — dependent pehle maarta hai us cheez se jis par woh depend karta hai.
Verify: destruction sequence [2, 1] hai, construction [1, 2] ka exact reverse. ✅
Worked example Example 8 — Cell H: real-world word problem (file + lock saath mein)
Statement. Ek bank server function ko: (1) ledger protect karne wala mutex lock karna hai, (2) ek log file open karni hai, (3) credit() call karni hai jo throw kar sakta hai . RAII se, prove karo ki credit() se ek throw na mutex locked chhod ta hai na file open.
void record_transfer () {
std ::lock_guard < std ::mutex > lk (m); // lock acquire karo: locks 0 -> 1
std ::ofstream log ( "audit.txt" ); // file acquire karo: files 0 -> 1
credit (); // yahan throw ho sakta hai
} // unwinding: ~log file close karta hai (files -> 0), ~lk unlock karta hai (locks -> 0)
Forecast: agar credit() throw kare, toh baad mein locks aur files kya honge?
Lock acquire karo, phir file. locks = 1, files = 1. Yeh step kyun? Do resources do stack objects ke paas owned hain.
credit() throw karta hai. Yeh step kyun? Yeh Ex 2 (exception) ko Ex 7 (multiple owners) ke saath combine karta hai.
Reverse-order unwinding: ~ofstream file close karta hai (files = 0), phir ~lock_guard unlock karta hai (locks = 0). Yeh step kyun? Dono destructors unwinding ke dauran fire hote hain, LIFO order mein — koi leak nahi, koi deadlock nahi.
Verify: throw ke baad dono counters 0 hain. RAII ke bina mutex locked rehta (locks = 1 → future deadlock) aur file leak hoti (files = 1). ✅
Worked example Example 9 — Cell I: exam twist (throwing destructor)
Statement. Exception E1 se caused stack unwinding ke dauran, ek local object ka destructor khud exception E2 throw karta hai. Program kya karta hai, aur exception-safe fix kaise kaam karta hai?
struct Bomb {
~Bomb () noexcept ( false ) { throw std :: runtime_error ( "boom" ); } // BAD
};
void f () {
Bomb b;
throw std :: runtime_error ( "first" ); // E1 -> unwinding -> ~Bomb throws E2
}
Forecast: ek saath do exceptions in flight — kya hota hai?
E1 throw hota hai , unwinding begin hoti hai. Yeh step kyun? "Already handling an exception" state set up hoti hai.
~Bomb mid-unwind E2 throw karta hai. Yeh step kyun? Runtime ke paas ab do live exceptions hain aur unhe resolve karne ka koi defined tarika nahi.
Result: std::terminate() call hota hai → program abort. Yeh step kyun? Standard forbid karta hai ki unwinding ke dauran ek doosra exception destructor se escape kare.
Fix: destructors ko noexcept banao aur release ke dauran errors ko throw karne ki jagah swallow ya log karo:
~ Safe () noexcept { try { risky_close (); } catch (...) { /* log, don't rethrow */ } }
Verify: ek saath in flight allowed exceptions ki tadaad 1 hai; doosra ek terminate trigger karta hai — toh ek correct release path 0 exceptions throw karta hai. ✅
Recall Har danger matrix ka kaunsa cell hai?
Exception par leak ::: Cell B — stack unwinding destructor run karke fix karta hai.
if(...) return; par skipped cleanup ::: Cell C — har exit path destructor run karta hai.
Same address do baar free ::: Cell D — raw-pointer owner ka default copy; Rule of Five / unique_ptr se fix karo.
Emptied object safely destroy ho sakta hai ::: Cell F — destructor null case guard karta hai (no-op).
Cleanup se std::terminate ::: Cell I — unwinding ke dauran throwing destructor; use noexcept banao.
Ek resource correctly managed hai iff uska live counter 0 par khatam ho aur kabhi negative na jaaye . Positive = leak, negative = double-free.