5.2.29 · D4 · HinglishC++ Programming

ExercisesException safety — basic, strong, no-throw guarantees

2,999 words14 min read↑ Read in English

5.2.29 · D4 · Coding › C++ Programming › Exception safety — basic, strong, no-throw guarantees

Ek quick reminder us ladder ka jis pe hum grade kar rahe hain:

Figure — Exception safety — basic, strong, no-throw guarantees

Har exercise ka answer in chaar rungs mein se ek hoga, ya ek code fix jo tumhe ladder pe upar le jaaye. Yeh picture apne dimaag mein rakho: basic = koi leak nahi lekin values badal sakti hain; strong = sab kuch ya kuch nahi; no-throw = fail hi nahi ho sakta.


Level 1 — Recognition

Goal: ek chhoti function di gayi hai, uski guarantee ka naam batao. Abhi koi code likhna nahi — bas padho aur classify karo.

Recall Solution 1.1

Answer: basic (aur actually effectively no-effect) guarantee RAII ke through. int ek unique_ptr ka owned hai. Agar mayThrow() throw karta hai, toh stack unwind hota hai aur ~unique_ptr run karta hai, int ko free karta hai. Koi leak nahi. Kyunki g ka koi aur observable side effect nahi hai aur koi bhi allocated resource clean up ho jaata hai, visible program state unchanged rehta hai — lekin jo guarantee hum ek general body ke liye promise kar sakte hain woh basic hai: valid, destructible, koi leak nahi. Cleanup scope exit se tied hai, last line tak pahunchne se nahi — yahi RAII ka poora point hai.

Recall Solution 1.2
  1. No-throw. Pointers exchange karna koi allocation nahi touch karta aur throw nahi kar sakta. (Yahi reason hai ki swap copy-and-swap ka commit step hai.)
  2. Strong. push_back ya toh successfully grow aur insert karta hai, ya throw karta hai aur vector ko bilkul pehle jaisa chhhod deta hai.
  3. No-throw. Ek well-behaved destructor kabhi throw nahi karta (dekho Stack unwinding and destructors).
  4. Basic. Log write ek side effect hai jo persist karta hai chahe baad wala step throw kare — dono steps mein koi rollback nahi hai, isliye poora sirf basic hai.

Level 2 — Application

Goal: code fix karne ya target guarantee tak pahunchne ke liye RAII, copy-and-swap, aur noexcept apply karo.

Recall Solution 2.1

Raw owning pointer ko unique_ptr se replace karo:

void fixed() {
    auto w = std::make_unique<Widget>();
    w->configure();   // may throw — ~unique_ptr still frees w
}                     // basic guarantee, no leak

Kyun: agar configure() throw karta hai, toh original mein manual delete w; skip ho jaata hai → leak. Smart pointer ka destructor unwinding ke dauran run karta hai regardless of kahan control function se nikalti hai.

Recall Solution 2.2
void addTagStrong(std::vector<std::string>& tags, std::string t) {
    std::vector<std::string> tmp = tags;   // copy (may throw → tags safe)
    tmp.push_back(std::move(t));           // work on the copy
    validate(tmp);                         // may throw → tags still untouched
    tags.swap(tmp);                        // noexcept commit
}

Kyun: original mein, (A) succeed hota hai aur phir (B) throw kar sakta hai, tag already pushed hua tha → sirf basic. Fix mein, har throwing step sirf tmp ko touch karta hai. Visible tags sirf swap pe change hota hai, jo noexcept hai. Toh ya toh sab kuch commit hota hai ya tags bilkul pehle jaisa hai → strong.


Level 3 — Analysis

Goal: reason karo ki ek subtle case kyun fail hota hai, exactly woh line dhundho jo guarantee todti hai.

Recall Solution 3.1

Do problems hain, dono intended commit point ke baad.

  • Line (Y) ek throwing operation hai jo state change ke baad place ki gayi hai. Agar notify() throw karta hai, cfg already update ho chuki hai → operation ka effect hua → strong nahi.
  • Line (X) move-assign use karta hai, jo commit hai. Yeh true no-throw commit hone ke liye, Config ka move assignment noexcept hona chahiye. Agar yeh throw kar sakta hai, toh commit khud mid-swap fail ho sakta hai → strong guarantee khatam. Fix: commit ko noexcept swap banao aur uske baad kuch bhi throwing mat rakho:
void updateStrong(Config& cfg, Settings s) {
    Config tmp = cfg;
    tmp.apply(s);        // all throwing work here
    notify_prepare(tmp); // any throwing prep also on tmp
    swap(cfg, tmp);      // noexcept — the last line
}
Recall Solution 3.2

Haan — push_back strong guarantee preserve karta hai, lekin ek cost pe. Kyunki T ka move ctor throw kar sakta hai, library std::move_if_noexcept rule apply karti hai: woh elements ko new buffer mein copy karti hai instead of move karne ke. Dekho std::vector reallocation strategy. Kyun: agar woh move karti aur ek move beech mein throw karta, toh old buffer already partly gutted hota → rollback impossible → strong khatam. Copying har source element ko intact rakhti hai, isliye agar ek copy throw kare toh library new buffer discard kar sakti hai aur old wale ko untouched rakh sakti hai. Iska price hai ek slower copy instead of cheap move.


Level 4 — Synthesis

Goal: naya correct code banao jo tools ko compose kare.

Recall Solution 4.1
Buffer& Buffer::operator=(Buffer rhs) {   // rhs = a copy, made by the caller
    swap(*this, rhs);                     // noexcept commit
    return *this;                         // ~rhs frees the old data
}

Kyun kaam karta hai:

  • (Possibly throwing) copy argument passing ke dauran hoti hai, body shuru hone se pehle. Agar woh throw kare, *this kabhi touch nahi hua → strong.
  • Body sirf ek noexcept swap hai → commit fail nahi ho sakta.
  • Self-assignment safe for free: even b = b pehle rhs mein copy karta hai, phir *this ko us independent copy se swap karta hai — koi aliasing hazard nahi, koi special if (this == &rhs) check ki zaroorat nahi.
Recall Solution 4.2

Saari mutation staged copies pe karo, phir dono accounts ko ek saath no-throw swap se commit karo:

void transfer(Account& A, Account& B, Money amount) {   // STRONG
    Account tmpA = A;               // copies (may throw → A,B untouched)
    Account tmpB = B;
    tmpA.debit(amount);             // may throw (insufficient funds, etc.)
    tmpB.credit(amount);            // may throw
    using std::swap;                // commit both, no-throw:
    swap(A, tmpA);                  // noexcept
    swap(B, tmpB);                  // noexcept
}

Kyun: saari validation aur arithmetic tmpA/tmpB pe run hoti hai. Agar inme se koi bhi throw kare, real A aur B kabhi modify nahi hue → koi effect nahi. Dono commits noexcept swaps hain, aur jab pehla swap run hota hai toh doosra fail nahi ho sakta, isliye hum kabhi pair ko half-transferred nahi chhodte. (Dono swaps milke atomic commit banate hain.)


Level 5 — Mastery

Goal: ek library author ki tarah noexcept, std::terminate, aur design trade-offs ke baare mein sochna.

Recall Solution 5.1

Allocation std::bad_alloc throw karta hai. Kyunki function noexcept declare hai, runtime exception ko propagate nahi karta — instead woh std::terminate() call karta hai, jo by default program ko koi stack unwinding ke bina abort karta hai (dekho noexcept specifier and std::terminate). Locals ke destructors nahi run karte; program instantly mar jaata hai. Kyun: noexcept ek promise hai. Agar ek noexcept function ek exception escape karne de, toh language isko ek unrecoverable contract violation samajhti hai. Fix: ek real swap existing handles/pointers exchange karta hai aur kuch bhi allocate nahi karta:

friend void swap(T& a, T& b) noexcept {
    using std::swap;
    swap(a.ptr_, b.ptr_);   // exchange pointers, no allocation
    swap(a.n_,   b.n_);
}

Sirf wahi noexcept declare karo jiske baare mein tum sach mein guarantee kar sako ki kabhi throw nahi karega.

Recall Solution 5.2

Ek jhooth bolne wale noexcept move ke saath, std::vector reallocation ke dauran elements ko move karega (std::move_if_noexcept ke per, jo tumhari declaration pe trust karta hai). Agar phir ek move throw kare, toh ek noexcept function se escape std::terminate() call karta hai → program death. Contrast karo: honestly not noexcept chhoda hota, toh vector copy karta instead, strong guarantee maintain karta aur sirf slow chalta. Toh jhooth bolna ek recoverable, strong-guaranteed slowdown ko ek fatal crash mein convert karta hai. "Optimisation" correctness ko speed ke liye trade karta hai aur correctness catastrophically kho deta hai.

Recall Solution 5.3

(a) ≈ (b) > (c).

  • (a) noexcept pointer swap — throw nahi kar sakta, kuch allocate nahi karta: gold standard commit.
  • (b) noexcept move-assign — yeh bhi throw nahi kar sakta provided noexcept honest hai; commit ke roop mein equally valid, aur often wahi hai jo copy-and-swap mein compile hota hai. Verify karne ke liye thoda zyada hai (poora move no-throw hona chahiye).
  • (c) throwing copy-assignbilkul valid commit nahi hai. Agar copy-assign mid-write throw kare, toh target partly modified ho sakta hai → strong guarantee khatam. Commit step no-throw hona chahiye; (c) yeh requirement fail karta hai.

Recall Feynman recap — poori ladder ek saanch mein

Basic = RAII clean up karta hai, kuch leak nahi hota, lekin values badal sakti hain. Strong = messy kaam ek copy pe karo, phir isse last no-throw instant pe swap karo, toh ya sab commit hota hai ya kuch nahi. No-throw = itna simple ki fail hi nahi ho sakta, aur tum isse noexcept se prove karte ho — lekin agar jhooth bolo, toh program mar jaata hai. Tool ladder: RAII → copy-and-swap → noexcept.


Active Recall

Copy-and-swap ka commit step noexcept kyun hona chahiye?
Taaki atomic all-or-nothing property hold kare — agar commit throw kar sakta, toh tum ek half-committed, unrecoverable state mein phans sakte ho.
std::vector reallocation ke dauran kya karta hai jab element ka move ctor noexcept nahi hai?
Woh move karne ki jagah copy karta hai (std::move_if_noexcept), taaki ek throw old buffer ko gut nahi kar sake, strong guarantee preserve karta hai.
Agar ek noexcept function ek exception escape karne de toh kya hota hai?
Runtime std::terminate() call karta hai, jo bina stack unwinding ke abort karta hai — locals ke destructors run nahi karte.
operator=(Buffer rhs) free mein self-assignment-safe kyun hai?
Copy argument passing ke dauran body se pehle banti hai, isliye even b = b *this ko ek independent copy se swap karta hai — koi aliasing hazard nahi.