5.2.13 · D4 · HinglishC++ Programming

ExercisesRAII — resource acquisition is initialization — why it's the key idiom

3,548 words16 min read↑ Read in English

5.2.13 · D4 · Coding › C++ Programming › RAII — resource acquisition is initialization — why it's the

Do ideas jinpar tum baar baar lean karoge, seedhe shabdon mein:

  • Destructor — ek special member jo automatically tab run hota hai jab object khatam ho jaata hai (apna scope chhod deta hai). Dekho Constructors and Destructors.
  • Stack unwinding — jab koi function kisi bhi wajah se exit hota hai (return, break, ya ek thrown exception), tab tak jo bhi local objects bane hain unhe reverse order mein destroy kiya jaata hai, aur unke destructors run hote hain. Dekho Stack Unwinding and Exceptions.

Neeche sab kuch anhi do facts ka application hai, baar baar harder hota hua.


Level 1 — Recognition

L1.1 — Memory kaunsi line free karti hai?

void demo() {
    auto p = std::make_unique<int>(42);   // (A)
    use(*p);                              // (B)
}                                         // (C)

Question: heap par jo int hai wo kis labelled point par release hota hai?

Recall Solution

(C) par — closing brace par. Exactly wahan kyun? p ek local object hai demo ka, isliye uski lifetime us waqt khatam hoti hai jab control demo ke scope se bahar jaata hai — aur woh scope closing brace (C) par khatam hota hai. C++ guarantee deta hai ki jab kisi local object ka scope khatam hota hai, tab uska destructor usi point par run hota hai; unique_ptr ka destructor hi woh cheez hai jo managed int par delete call karta hai. Isliye release (C) se pinned hai kyunki wahan p marta hai, aur death woh waqt hai jab destructor (release code) fire hota hai. Koi explicit delete line exist nahi karti precisely isliye kyunki destructor hi release ka moment hai. Dekho Smart Pointers - unique_ptr shared_ptr.

L1.2 — RAII wrapper ka naam batao

Har raw resource ke liye, standard RAII type ka naam batao jo usse own karti hai.

Raw resource RAII type?
single heap object, one owner ?
heap object, shared owners ?
a mutex lock ?
a growable byte buffer ?
Recall Solution
  • single owner → std::unique_ptr<T>
  • shared → std::shared_ptr<T> (reference-counted)
  • mutex lock → std::lock_guard<std::mutex> (dekho std::lock_guard and Mutexes)
  • growable buffer → std::vector<char> (ya std::string)

std::vector<char> RAII kyun qualify karta hai? Iska constructor acquire karta hai: vector allocator ko call karta hai (effectively new[]) taaki apne elements ke liye heap buffer grab kar sake. Iska destructor release karta hai: jab vector scope se bahar jaata hai, uska destructor woh buffer free kar deta hai (effectively delete[]) automatically — tumhe kabhi delete[] khud nahi likhna padta. Isliye ek vector ek chhota owning object hai jo raw buffer ke around wrap hai: acquire-in-constructor, release-in-destructor, lifetime tied to scope. Yahi exactly RAII pattern hai, isliye yeh manual new[]/delete[] ko replace karta hai.

Common thread: acquire in constructor, release in destructor — isliye scope, manual call nahi, cleanup control karta hai.


Level 2 — Application

L2.1 — Leaky function ko convert karo

Rewrite karo taaki might_throw() throw kare tab bhi koi leak na ho.

void f() {
    int* p = new int[100];
    might_throw();
    delete[] p;
}
Recall Solution
void f() {
    std::vector<int> v(100);   // acquire in constructor
    might_throw();             // throws? v's destructor still runs
}                              // release, always

Kyun: new int[100] + delete[] ek manual acquire/release pair hai; throw delete[] ke upar se jump kar jaata hai. std::vector buffer ko ek object ke andar rakhta hai, isliye stack unwinding use free kar deta hai chahe f kisi bhi tarah exit kare.

L2.2 — Destructor calls count karo

struct Loud {
    int id;
    Loud(int i): id(i) {}
    ~Loud() { /* prints id */ }
};
void g() {
    Loud a(1);
    Loud b(2);
    Loud c(3);
}

Destructors kis order mein run karte hain, aur total kitne run hote hain?

Recall Solution

Total = 3 destructor calls. Order = 3, 2, 1 (construction ka reverse — LIFO).

Ek purely textual trace, agar neeche ka figure render na kare:

event id 1 (a) id 2 (b) id 3 (c)
construct (top→bottom = latest→earliest) 1st built 2nd built 3rd built
destroy order 3rd (last) 2nd 1st (first)

Isliye construction 1, 2, 3 chalti hai aur destruction 3, 2, 1 — exactly reverse.

Figure wahi baat visually dikhata hai. Kya observe karna hai: left side par, teeno objects build order mein stack hain — a (id 1) pehle create hota hai aur bottom mein baithta hai, phir b (id 2), phir c (id 3) upar; upar ki arrow construction ki direction hai. Right side par, wahi stack top se neeche khali hoti hai: ~c pehle run karta hai, phir ~b, phir ~a; neeche ki arrow destruction ki direction hai. Donon arrows opposite directions point karte hain — woh opposition hi LIFO (last-in-first-out) rule hai. Har box apne id se bhi label hai, taaki tum colour par rely kiye bina usse follow kar sako.

Figure — RAII — resource acquisition is initialization — why it's the key idiom

Level 3 — Analysis

L3.1 — Double-free dhoondhna

class Buf {
    int* p;
public:
    Buf(int n): p(new int[n]) {}
    ~Buf() { delete[] p; }
    // no copy/move written
};
void bad() {
    Buf a(10);
    Buf b = a;   // (*)
}

Scope end par exactly kya galat hota hai, yeh explain karo.

Recall Solution

Compiler (*) par ek default copy constructor generate karta hai, jo pointer value p ko copy karta hai. Ab a.p aur b.p dono same address hold karte hain. Scope end par dono destructors run karte hain: b ka delete[] block free karta hai, phir a ka delete[] same, already-freed block free karta hai → double free (undefined behaviour). Dekho Rule of Three Five Zero.

Fix (Rule of Five): ya toh copy operations ko delete karo, buffer ko deep-copy karo, ya data ko std::vector<int> mein store karo (jo correctly copy karta hai aur kisi hand-written destructor ki zaroorat nahi).

L3.2 — Mutex unlock kahan hoga?

std::mutex m;                                // the shared mutex
 
void transfer() {
    std::lock_guard<std::mutex> lk(m);   // (A) acquire the lock on m
    debit();
    credit();     // throws here
    log();        // (never reached)
}                 // (B)

credit() throw karta hai. Kya mutex locked rehta hai? Woh kis point par unlock hota hai?

Recall Solution

Mutex unlock hota hai(B) par, stack unwinding ke dauran. Jab credit() throw karta hai, exception transfer se bahar propagate hoti hai; scope chhodne se pehle, local lk destroy hota hai, aur lock_guard ka destructor m.unlock() call karta hai. RAII ke bina credit() ke baad ek manual unlock() skip ho jaata, mutex forever locked chhod kar → deadlock. Dekho std::lock_guard and Mutexes aur Exception Safety Guarantees.

L3.3 — Unwinding ke dauran throwing destructor

struct Risky {
    ~Risky() { throw std::runtime_error("boom"); }  // destructor throws
};
void h() {
    Risky r;              // built first
    throw std::logic_error("first");   // starts unwinding
}                         // ~Risky runs during unwinding... then throws

Stack already unwind ho raha tha jab ~Risky run hota hai aur woh bhi throw karta hai. Program kya karta hai, aur kyun?

Recall Solution

Program std::terminate call karta hai (jo by default abort karta hai). Kyun: jab throw std::logic_error("first") fire hota hai, stack unwinding shuru hoti hai aur ~Risky ek exception already in flight rehte run karta hai. Agar uss unwinding ke dauran destructor ek doosra exception throw kare, toh C++ ke paas do live exceptions hain aur koi rule nahi ki kaun sa propagate ho — isliye language give up kar ke std::terminate call karta hai. Fix / rule: ek release-only destructor ko kabhi exception escape nahi karne dena chahiye; use noexcept mark karo aur andar koi bhi error swallow ya log karo. Isliye neeche jo bhi RAII destructor tum likho woh noexcept marked hota hai. Dekho Stack Unwinding and Exceptions aur Exception Safety Guarantees.


Level 4 — Synthesis

L4.1 — Move-only file wrapper banao

Ek class File likho jo ek FILE* own kare, constructor mein open kare (failure par throw kare), destructor mein close kare, copying forbid kare, aur moving support kare.

Recall Solution
class File {
    FILE* f;
public:
    explicit File(const char* name)                 // (1) acquire
        : f(std::fopen(name, "r")) {
        if (!f) throw std::runtime_error("open failed");
    }
    ~File() noexcept { if (f) std::fclose(f); }     // (2) release, always, noexcept
 
    File(const File&)            = delete;          // (3) no copy
    File& operator=(const File&) = delete;          // (4)
 
    File(File&& o) noexcept : f(o.f) { o.f = nullptr; }   // (5) steal + null
    File& operator=(File&& o) noexcept {
        if (this != &o) {
            if (f) std::fclose(f);   // release our old handle first
            f = o.f;                 // steal
            o.f = nullptr;           // leave source empty
        }
        return *this;
    }
    FILE* get() const { return f; }
};

Har piece kyun hai: constructor acquisition ko birth se tie karta hai; destructor release ko death se tie karta hai (aur noexcept marked hai — L3.3 yaad karo: ek release-only destructor jo stack unwinding ke dauran throw kare std::terminate call karta hai); copy delete hai taaki do Files kabhi same handle ko fclose na kar sakein; move pointer steal karta hai aur source null karta hai taaki exactly ek owner kabhi bhi close kare. Move-assignment ko pehle apna current handle close karna chahiye (otherwise woh leak ho jaayega) steal karne se pehle. Dekho Move Semantics aur Rule of Three Five Zero.

L4.2 — Ownership walkthrough

Step by step describe karo ki owned handle ke saath kya hota hai jab: A construct karo, phir B = std::move(A), phir scope ka end.

Recall Solution

Single handle H ko har event ke through trace karo:

  1. A construct karo. A handle H open/acquire karta hai. State: A owns H; B abhi exist nahi karta.
  2. B = std::move(A). Move constructor steal karta hai: B.f = A.f (ab B H own karta hai), phir source null karta hai: A.f = nullptr. State: B owns H; A nullptr hold karta hai.
  3. Scope end — destructors LIFO mein run karte hain, toh pehle B, phir A. ~B real handle dekhta hai aur H close karta hai (fclose). ~A nullptr dekhta hai, isliye uska if (f) guard false hai aur woh kuch nahi close karta.

Invariant preserved: poore sequence mein exactly ek acquire tha (step 1) aur exactly ek release (step 3, B ki taraf se). A double-close kabhi nahi karta kyunki move ne use null kar diya. Isliye "steal aur null" ek single indivisible step hai — source ko null karna hi woh cheez hai jo one-owner-one-release invariant ko true rakhti hai. Dekho Move Semantics.

Ownership ka wahi flow, plain text table mein likha (koi diagram ki zaroorat nahi):

event A holds B holds handle H
after construct A H open, owned by A
after B = move(A) nullptr H open, owned by B
scope end → ~B nullptr (dying) closed by ~B
scope end → ~A (dying, nullptr) gone already closed; ~A closes nothing

Table ko top se bottom padhte hue: H ki ownership A se B ko jaati hai, aur sirf B kabhi use close karta hai — ek single acquire aur ek single release.

move to B

A owns handle H

B owns handle H

A holds nullptr

scope ends B closes H

scope ends A closes nothing


Level 5 — Mastery

L5.1 — Reference count predict karo

auto p = std::make_shared<int>(7);   // (A) count = ?
{
    auto q = p;                      // (B) count = ?
    auto r = std::move(q);           // (C) count = ?  what is q now?
}                                    // (D) count = ?
// (E) count = ?

Har point par reference count aur (C) ke baad q ki state batao.

Recall Solution
  • (A) count = 1 — sirf p.
  • (B) count = 2q ek copy hai, isliye woh ownership share karta hai (count increment hota hai).
  • (C) count = 2 — ek move q ka share r ko transfer karta hai; owners ki total sankhya unchanged rehti hai. Move ke baad q empty hai (q == nullptr), isliye woh koi share hold nahi karta.
  • (D) inner scope ka end: r aur q destroy hote hain. q empty tha (koi decrement nahi); r apna share release karta hai → count = 1.
  • (E) count = 1 — sirf p bacha hai.

Heap int sirf tab free hota hai jab count 0 tak pahunche, yaani jab p finally mare. Copy count badhata hai; move ek share relocate karta hai bina use change kiye. Dekho Smart Pointers - unique_ptr shared_ptr.

L5.2 — Strong exception-safety guarantee

Ek function do side effects karta hai jo dono succeed karni chahiye ya dono nahi. Kaun sa RAII pattern strong guarantee deta hai (failure par, state unchanged rehti hai), aur "commit last" kyun matter karta hai?

Recall Solution

Pattern: pehle saara woh kaam karo jo throw kar sakta hai ek side copy / staging area par, phir ek single non-throwing commit (ek swap ya noexcept move) bilkul last step ke roop mein perform karo. Agar koi early step throw kare, toh stack unwinding staging RAII objects destroy kar deta hai aur original state bilkul untouched rehti hai — yahi strong guarantee hai (failure par, observable state exactly wahi hai jaisi call se pehle thi).

"Commit last" hi poora trick kyun hai: commit woh single step hai jo real state mutate karta hai, aur ise ek aisi operation choose kiya jaata hai jo throw nahi kar sakti (pointers ka swap, ya noexcept move). Kyunki woh throw nahi kar sakta, ek baar use reach karo toh success certain hai — koi half-mutated state possible nahi. Commit se pehle sab kuch sirf throwaway copy ko touch karta hai, isliye wahin failure copy discard kar deta hai aur kuch real change nahi hota.

Contrast — basic guarantee: agar instead tum real state ko directly mutate karo aur exception halfway fire kare, toh tumhe weak basic guarantee milti hai: koi leak nahi aur koi corruption nahi (saare invariants still hold, resources RAII se free), lekin object ek valid-yet-changed state mein reh sakta hai — kuch kaam hua, kuch nahi. Basic = "kuch leak nahi, still usable"; strong = "kuch leak nahi aur aisa lagta hai jaise call hua hi nahi." Commit-last-with-a-noexcept-step recipe precisely wahi hai jo basic ko strong mein upgrade karti hai. Dekho Exception Safety Guarantees.

L5.3 — Order-of-destruction dependency puzzle

void h() {
    Logger log;          // opens a log file
    Session s(log);      // s writes to log during its own destruction
}

Kya yeh safe hai? Destruction order use karke justify karo.

Recall Solution

Safe hai. log pehle construct hota hai, s doosra. Destruction reverse order mein hai: pehle s, phir log. Isliye jab ~Session run hota hai aur apna final message likhta hai, log still alive hai (uska destructor abhi run nahi hua). Dependency "s depends on log" precisely isliye honoured hoti hai kyunki depended-upon object (log) last destroy hota hai. Reverse-order destruction exactly wahi hai jo aisi dependency chains ko valid banata hai. Declarations ka order badal dena (Session ko Logger se pehle) ise break kar dega.


Active recall

Recall Rapid self-check
  • L1.1 mein, koi explicit delete kyun nahi hai? (destructor hi release ka moment hai)
  • L2.2 mein, kitne destructors run hote hain aur kis order mein? (3; ids 3,2,1)
  • L3.3 mein, kya hota hai agar destructor unwinding ke dauran throw kare? (std::terminate)
  • L3.1 mein, kaun sa UB hota hai aur kyun? (double free — shared pointer value)
  • L5.1 mein, (C) par count kya hai aur q ki state kya hai? (2; q empty hai)
  • L5.2 mein, basic ko strong mein kya upgrade karta hai? (copy par kaam karo, last mein noexcept swap/move se commit karo)
  • L5.3 mein, ~Session se log mein likhna safe kyun hai? (log, s se zyada jeeta hai — last mein destroy hota hai)