5.2.29 · D3 · Coding › C++ Programming › Exception safety — basic, strong, no-throw guarantees
Intuition Yeh page kya hai
Parent note ne tumhe ladder diya tha: basic → strong → no-throw . Yahan hum drill karte hain: har woh shape jisme code ho sakta hai jab exception fire ho, aur usse classify ya fix kaise karein. Isse ek checklist samjho — is page ke baad, koi bhi thrown exception tumhe surprise nahi karna chahiye.
Jaise ek angle formula mein quadrants hote hain jahan naive version fail karta hai, exception safety mein bhi "cells" hote hain — recurring situations jo alag-alag answer maangti hain. Neeche ki figure hamara board hai: har cell ek square hai, colour se pata chalta hai ki kaunsa tool usse fix karta hai, bilkul waise jaise angle ke chaar quadrants chart karte ho.
The scenario board. Har square ek cell hai (C1–C9); uska colour batata hai kaunsa tool apply hota hai.
Definition Ek word jo baar baar aayega:
acquisition
Ek resource acquire karna matlab hai kisi aisi cheez ki ownership lena jo program ko baad mein release karni padegi — new se memory allocate karna, file kholna, mutex lock karna. Dangerous window hai "acquired" aur "released" ke beech: agar exception is beech mein fire ho, toh koi to us resource ko release karega. Cell C2 neeche ek doosre acquisition ke baare mein hai jo throw karta hai jab pehla abhi bhi un-released hai.
Yeh pura board words mein:
Cell
Situation
Core question
C1
Single resource, acquire ke baad throw ho sakta hai
Kya cleanup phir bhi hoga?
C2
Do resources, doosra acquisition (allocation) throw karta hai
Kya pehla leak hoga?
C3
Multi-step visible mutation, baad wala step throw karta hai
Kya pehle wale steps roll back honge?
C4
C3 ka copy-and-swap upgrade
Kya hum isse strong bana sakte hain?
C5
"Zero / degenerate" input (empty, self, no-op)
Kya safe path trivial input survive karta hai?
C6
Move jo mid-transfer throw karta hai
Kya strong guarantee kho jaati hai? Copy vs move.
C7
noexcept jo jhoot bole aur throw kare
Runtime pe actually kya hota hai?
C8
Real-world word problem (bank transfer)
Business ko kaunsi guarantee chahiye?
C9
Exam twist: kaunsi guarantee, aur kyun stronger nahi?
Pure picture ke baare mein soch, parts ke baare mein nahi.
Neeche, har example cell(s) ke saath tagged hai. Milake ye pura board cover karte hain.
Worked example C1 — ek resource, kaam throw kar sakta hai
void draw () {
auto pen = std :: make_unique < Pen >(); // acquire
pen-> stroke (); // may throw
}
draw kaunsi guarantee deta hai, aur kyun?
Forecast: Abhi guess karo — basic, strong, ya no-throw? Agar stroke() throw kare toh kuch leak hoga kya?
Steps:
make_unique<Pen>() allocate karta hai aur ownership pen ko deta hai, jo ek stack object hai.
Yeh step kyun? Stack pe unique_ptr anchor hai — iska lifetime scope se tied hai, last line tak pahunchne se nahi. Dekho Smart pointers — unique_ptr, shared_ptr .
stroke() run hota hai aur throw karta hai.
Yeh step kyun? Hum failure path test kar rahe hain — safety ke liye sirf yahi path matter karta hai.
Stack unwind hoti hai; ~unique_ptr run hota hai aur Pen ko delete karta hai.
Yeh step kyun? Stack unwinding and destructors guarantee karta hai ki fully-constructed locals ke destructors run honge. Koi leak nahi.
Answer: Basic guarantee (via RAII and resource management ). Strong nahi, kyunki draw ke paas koi visible state roll back karne ke liye hai hi nahi — lekin agar usne kuch partial visible work kiya hota, toh woh persist karta.
Verify: new vs delete count karo. Ek allocation, exactly ek deletion unwinding ke dauran → net zero live objects. Koi leak nahi. ✓
Worked example C2 — classic leak trap
void bad () {
Widget * a = new Widget (); // #1 acquisition ok
Widget * b = new Widget (); // #2 acquisition THROWS bad_alloc
delete a; delete b; // kabhi reach nahi hote
}
Line #2 throw kare toh kya leak hota hai?
Forecast: Kitne Widgets leak honge — zero, ek, ya do?
Steps:
a ke liye new Widget() succeed hota hai — ek live raw pointer, koi owner nahi. (Yeh pehla acquisition hai — memory ab kisi ki nahi sirf raw pointer ke.)
Yeh step kyun? Raw new ke saath koi destructor scope se attached nahi; kuch bhi automatically free nahi karega isse.
b ke liye new Widget() std::bad_alloc throw karta hai.
Yeh step kyun? Exception b assign hone se pehle fire hoti hai, toh b kabhi exist nahi karta.
Unwinding sirf stack objects ke destructors run karti hai. a ek raw pointer hai (ek value), owning object nahi — iska destructor kuch nahi karta.
Yeh step kyun? Raw pointers ke trivial destructors hote hain; pointee ko touch nahi kiya jaata.
Answer: Exactly ek Widget (woh jis par a point karta hai) leak hota hai. Is function ki koi guarantee nahi .
Fix (back to basic):
void good () {
auto a = std :: make_unique < Widget >();
auto b = std :: make_unique < Widget >(); // agar yeh throw kare, ~a a ko free karta hai
}
Verify: good mein, b ka allocation shuru hone se pehle a fully constructed hai; jab b throw kare, ~a run hota hai → 1 alloc, 1 free → 0 leaks. ✓
Worked example C3 — baad wala step throw karta hai, pehla persist karta hai
void append_and_log ( std :: vector < int > & v , int x ) {
v. push_back (x); // (A) visible state mutate karta hai
log_to_file (x); // (B) throw kar sakta hai
}
Kaunsi guarantee?
Forecast: push_back akela strong hai. Kya isse ek throwing log ke saath compose karne pe strong rehta hai?
Steps:
push_back(x) succeed hota hai → v mein ab ek extra element hai, caller ko visible .
Yeh step kyun? Yeh caller-visible object mein ek committed change hai.
log_to_file(x) throw karta hai.
Yeh step kyun? Commit already hone ke baad failure path test kar rahe hain.
Unwinding v ko pushed element ke saath chodti hai — koi rollback exist nahi karta.
Yeh step kyun? Kisine bhi v ka purana state save nahi kiya; usse restore karne ke liye kuch nahi hai.
Answer: Sirf Basic . v valid aur leak-free hai, lekin push persist hua → "no effect" nahi, toh strong nahi. Poora utna hi strong hota hai jitna tumhara rollback sare steps mein ho.
Verify: Pehle: v = [1,2]. x=3 ke saath call karo, log throw karta hai. Baad mein: v = [1,2,3] → length 2 se 3 ho gayi, toh effect ≠ none. ✓
Worked example C4 — copy-and-swap to the rescue
void append_and_log_strong ( std :: vector < int > & v , int x ) {
std ::vector <int> tmp = v; // 1. copy (throw kar sakta hai → v safe)
tmp. push_back (x); // 2. copy par kaam karo (throw kar sakta hai)
log_to_file (x); // 3. throw kar sakta hai → v abhi bhi untouched
v. swap (tmp); // 4. noexcept commit
}
Ab kaunsi guarantee?
Forecast: Kaunsi single line "point of no return" hai?
Steps:
v ko tmp mein copy karo. Agar copy throw kare, v untouched hai.
Yeh step kyun? Hum sari throwing work ek throwaway object par move kar dete hain. Dekho Copy-and-swap idiom .
2–3. push_back aur log_to_file throw kar sakte hain, lekin sirf tmp ko touch karte hain (log ka v par koi effect nahi).
Yeh step kyun? Jab tak failures v ko alter nahi kar sakte, v apni purani value rakhta hai = free mein rollback.
swap v aur tmp ke internal pointers exchange karta hai — yeh noexcept hai.
Yeh step kyun? Move semantics and noexcept : ek pointer swap allocate nahi kar sakta, toh throw nahi kar sakta → commit ek atomic, non-throwing instant hai.
Answer: Strong. v sirf line 4 par change hota hai, jo throw nahi kar sakta → commit-or-rollback.
Verify: Agar step 2 ya 3 throw kare: v abhi bhi apni input ke equal hai (maan lo [1,2]), unchanged. Agar kuch throw na kare: v = [1,2,3]. Do clean outcomes, koi in-between nahi. ✓
Definition Yahan use kiya gaya
Buffer type
Buffer ek chhota hand-rolled container hai jo ek raw pointer data aur size n ke through ints ka ek heap array own karta hai — wahi class jo parent note use karta hai. Iske paas hai: ek copy constructor (ek fresh array allocate karta hai aur elements copy karta hai — throw kar sakta hai ), ek noexcept swap friend jo data pointers aur n exchange karta hai, aur ek destructor jo data free karta hai. Hum isse yahan isliye use karte hain kyunki iska assignment copy-and-swap ke liye textbook stage hai.
Worked example C5 — empty vector, self-assignment, no-op
Example 4 ki strong function lo aur Buffer ke liye yeh copy-and-swap assignment lo:
Buffer & operator = ( Buffer rhs ) { // rhs = ek copy (pass-by-value)
swap ( * this , rhs);
return * this ; // ~rhs PURANA data free karta hai
}
Kya trivial inputs — empty container, self-assignment b = b, zero elements — kuch break karte hain?
Forecast: Self-assignment naive assignment operators ko break karta hai. Kya copy-and-swap isse survive karta hai?
Steps:
Empty container: copy constructor zero elements copy karta hai (kabhi throw nahi karta), swap commit karta hai.
Yeh step kyun? Safe path mein size 0 ke liye koi special case nahi hai — bas kaam karta hai, kyunki ek empty array copy karna ek no-op hai jo fail nahi ho sakta.
Self-assignment b = b: rhs b ki ek alag copy hai jo argument passing se bani hai, body run hone se pehle .
Yeh step kyun? Kyunki hum pehle copy karte hain, *this aur rhs alag objects hain — inhe swap karna harmless hai.
swap ke baad, rhs b ka purana buffer hold karta hai aur iska destructor isse ek baar free karta hai — koi double-free nahi.
Yeh step kyun? Ownership cleanly rhs mein chali gayi; exactly ek destructor purani memory release karta hai.
Answer: Sare degenerate inputs for free safe hain — yeh copy-and-swap ka ek headline benefit hai: self-assignment safety bina explicit if (this == &rhs) check ke.
Verify: b = b jahan b = [7]: copy rhs = [7] banata hai, swap *this = [7] aur rhs = [7] (purana) deta hai, ~rhs ek baar free karta hai → b still [7], ek free, zero double-frees. ✓
Worked example C6 — kyun vector moves ki bajaye copy karta hai
3 elements wala std::vector<T> ek bade buffer mein reallocate karta hai. Element #2 ko move ke zariye transfer karna throw karta hai.
Forecast: Agar moves throw kar sakte hain, toh kya strong guarantee bachi rehti hai ya kho jaati hai? Library isse avoid karne ke liye kya karti hai?
Neeche ki figure mein dono buffers stacked hain; isse top-to-bottom padho.
Throwing move ke saath reallocation: purana buffer half-gutted hai, toh roll back karne ke liye kuch intact nahi bachta.
Steps:
Vector naya, bada buffer allocate karta hai (dekho std::vector reallocation strategy ).
Yeh step kyun? Growth ke liye contiguous memory chahiye; purana buffer abhi ke liye alive rehta hai.
Elements ek-ek karke transfer hote hain. Agar moving ho , toh element #0 aur #1 purane buffer mein ab gutted hain (moved-from). Element #2 ka move throw karta hai.
Yeh step kyun? Move source se steal karta hai, toh purana buffer already damaged hai — figure mein greyed cells aur pink X dekho.
Rollback ab impossible hai: purane buffer mein half-empty (moved-from) slots hain. Strong guarantee lost.
Yeh step kyun? Tum woh restore nahi kar sakte jo move already steal kar chuka hai.
Fix: Agar T ka move constructor not noexcept ho, toh library copy karne lagti hai (std::move_if_noexcept). Copy source ko intact chodti hai → agar #2 ka copy throw kare, purana buffer untouched hai → strong preserved.
Yeh step kyun? Copy slow hai lekin non-destructive; yahan safety speed se zyada important hai.
Answer: Throw kar sakne wala move → strong lost , toh library copy karta hai. Apne move constructor ko noexcept mark karo (tabhi jab truly throw nahi kar sakta) fast move path unlock karne ke liye.
Verify (logic): noexcept(move) == true → library moves (fast). noexcept(move) == false → library copies (safe). Yeh decision exactly move_if_noexcept hai. ✓
Worked example C7 — the fatal lie
void release () noexcept {
throw std :: runtime_error ( "oops" ); // promise kiya tha nahi karenge!
}
Runtime pe kya hota hai?
Forecast: Kya exception normally caller tak propagate hoti hai? Ya kuch aur bura?
Steps:
Function noexcept declare hai, compiler aur runtime ke liye ek promise hai.
Yeh step kyun? Runtime is promise par rely karta hai — e.g. vector ka move path assume karta hai ki noexcept marked moves throw nahi karenge.
Ek throw actually noexcept boundary se escape karta hai.
Yeh step kyun? Hum deliberately contract break kar rahe hain consequence dekhne ke liye.
Runtime turant std::terminate() call karta hai — koi stack unwinding nahi , outer frames ke destructors run nahi karte.
Yeh step kyun? Dekho noexcept specifier and std::terminate : noexcept violate karna by design unrecoverable hai.
Answer: Program turant std::terminate se mar jaata hai . Ek normal exception se bhi bura, kyunki cleanup skip ho jaata hai. Rule: noexcept sirf un operations ke liye mark karo jo tum truly guarantee kar sako — swaps, moves, deallocation, destructors.
Verify: noexcept violation → std::terminate (catchable exception nahi). Koi catch block isse intercept nahi kar sakta. ✓
Definition Neeche use kiye gaye
Money aur Account types
Money ek simple value type hai jo cents ki ek whole number rakhta hai (ek integer count). Account ek Money balance store karta hai. Jo operations hum rely karte hain:
debit(amt) account ke balance se amt plain integer arithmetic se subtract karta hai — saste aur in-range values ke liye no-throw . Kyun matter karta hai: subtraction itself fail nahi ho sakta, toh yeh danger ka source nahi hai — lekin yeh ek committed visible change zaroor hai ek baar ho jaane ke baad.
credit(amt) balance mein amt add karta hai, lekin yahan yeh throw kar sakta hai (socho overflow check, ya andar credit mein downstream ledger/network write). Kyun matter karta hai: yeh pure example mein ek throwing step hai, toh yeh exactly woh point hai jahan hamari failure-path reasoning target karni chahiye.
Ek Account copy karna ek plain value copy hai jo invariant "balance ek valid integer hai" maintain karta hai. Kyun matter karta hai: kyunki copying kabhi state corrupt nahi karti, hum safely copies par sara transfer stage kar sakte hain (yahi copy-and-swap applicable banaata hai).
Worked example C8 — bank transfer
Account A se Account B mein $100 transfer karo:
void transfer ( Account & A , Account & B , Money amt ) {
A. debit (amt); // (1) A se subtract karo
B. credit (amt); // (2) B mein add karo — throw kar sakta hai (network, overflow…)
}
Balances start: A = $500, B = $200, amt = $100.
Forecast: Agar credit throw kare, toh system mein baad mein kitna paisa hai?
Steps:
A.debit(100) → A = $400. Committed visible change.
Yeh step kyun? Debit real aur immediate hai; caller A = $400 already observe kar sakta hai.
B.credit(100) throw karta hai.
Yeh step kyun? Yeh failure path hai jo hume analyse karni hai — doosra visible step pehle commit hone ke baad fail hota hai.
Koi rollback exist nahi karta → A = $400, B = $200. $100 air mein gayab ho gaya.
Yeh step kyun? Kisine bhi A ka purana balance save nahi kiya, toh debit persist karta hai jabki credit kabhi nahi hua → business invariant "total money conserved" broken hai. Basic (koi leak nahi, valid objects) yahan kaafi nahi hai.
Business requirement: money conservation strong guarantee demand karta hai. Commit-or-rollback ke saath rewrite karo:
void transfer_strong ( Account & A , Account & B , Money amt ) {
Account tmpA = A, tmpB = B; // copies (throw kar sakte hain → originals safe)
tmpA. debit (amt);
tmpB. credit (amt); // throw kar sakta hai → originals untouched
swap (A, tmpA); swap (B, tmpB); // noexcept commit (real code: ek atomic step)
}
Yeh step kyun? Har throwing action (credit, aur copies bhi) sirf tmpA/tmpB ko touch karte hain; A aur B sirf noexcept swaps par change hote hain → commit-or-rollback.
Answer: Basic yahan ek bug hai; domain ko strong chahiye. Total pehle = $700, total baad mein (success) = $700, total baad mein (throw) = $700. Money dono taraf conserved hai.
Verify: Success: A=400, B=300 → sum 700. Throw: A=500, B=200 → sum 700. Naive version on throw: A=400, B=200 → sum 600 ≠ 700 (bug). ✓
Worked example C9 — "kaunsi guarantee, aur kyun stronger nahi?"
template < class T >
void safe_replace ( std :: vector < T > & v , std :: size_t i , T value ) {
v. at (i) = std :: move (value); // (1) bounds-checked, phir assign
}
T ka move-assignment not noexcept hai. Batao: kaunsi guarantee, aur justify karo kyun stronger nahi hai.
Forecast: at() bad index par koi change hone se pehle throw karta hai. Lekin assignment itself ke baare mein kya?
Steps:
v.at(i) std::out_of_range throw karta hai agar i bounds se bahar ho — yeh kisi bhi element ko touch karne se pehle hota hai.
Yeh step kyun? Out-of-range throw v ko completely unchanged chodta hai → woh path akela strong hai.
Agar i valid ho, toh v[i] = std::move(value) T ka move-assignment run karta hai, jo mid-assignment throw kar sakta hai .
Yeh step kyun? Hume doosra reachable path analyse karna hai: ek valid index. Kyunki T ka move-assign noexcept nahi hai, yeh halfway throw kar sakta hai, v[i] ko ek moved-from / partially-updated state mein chodke — yahi path guarantee ko cap karta hai.
Woh partially-updated v[i] valid aur destructible hai (koi leak nahi) lekin iska value unspecified hai → basic , strong nahi.
Yeh step kyun? Humne ek strong path (bad index) aur ek basic path (throwing assign) compose kiye; sabse kamzor jeetta hai.
Answer: Overall Basic guarantee. Stronger isliye nahi kyunki ek throwing move-assignment in-place roll back nahi ho sakta — element v[i] already half-overwritten ho sakta hai aur koi saved copy restore karne ke liye nahi hai. Isse strong banane ke liye: element ki ek copy pe assign karo phir isse swap in karo, ya T ke liye noexcept move-assignment require karo.
Verify (logic): whole-function guarantee = min(guarantee of each reachable path) = min(strong, basic) = basic. Bad-index path: v unchanged (strong). Valid-index throwing path: v[i] unspecified but valid (basic). Minimum hai basic . ✓
Recall Kaunse cell ko kaunsa tool chahiye?
C1/C2 → RAII / Smart pointers — unique_ptr, shared_ptr . C3→C4 → Copy-and-swap idiom . C6 → noexcept moves + std::vector reallocation strategy . C7 → honest noexcept. C8 → domain invariants ke liye strong. C9 → whole = weakest path.
Mnemonic Pura board fill karo
"Leak, Rollback, Never-lie." Leaks (C1/C2) → RAII. Rollback (C3/C4/C8) → copy-and-swap. Never-lie (C6/C7) → truthful noexcept. C5 & C9 trap inputs hain — hamesha empty, self, aur mixed-strength composition test karo.
Bank transfer kyun strong chahiye, basic nahi? Kyunki basic objects ko valid rakhta hai lekin debit ko persist hone deta hai jabki credit fail ho — money conserved nahi hoti; domain invariant broken hai.
Example 2 mein exactly kitne Widgets leak hote hain aur kyun? Ek — a ka raw pointer koi destructor nahi rakhta, toh unwinding kabhi isse free nahi karti jab b ka new throw karta hai.
Copy-and-swap self-assignment-safe automatically kyun hai? Right-hand copy pass-by-value se body run hone se pehle banti hai, toh *this aur rhs hamesha alag objects hote hain.
std::vector kya karta hai jab T ka move constructor noexcept nahi hota?Woh reallocation ke dauran elements copy karta hai (move_if_noexcept), kyunki ek throwing move source ko gut kar deta aur strong guarantee kho deta.
Multi-path function ki whole-function guarantee kya hoti hai? Sare reachable paths mein sabse kamzor guarantee.