एक्सेप्शन: throw, try, catch
C++ के एक्सेप्शन मैकेनिज़्म को समझें — स्टैक अनवाइंडिंग, स्टैंडर्ड एक्सेप्शन हायरार्की, कस्टम एक्सेप्शन, और एरर कोड के बजाय एक्सेप्शन कब चुनना बेहतर है।
एक्सेप्शन क्यों होते हैं
एक्सेप्शन आने से पहले C कोड एरर बताने के लिए विशेष मान (जैसे -1 या NULL) लौटाता था। इसमें एक गंभीर कमी थी: कॉलर चुपचाप एरर को अनदेखा कर सकते थे। एक्सेप्शन इसका समाधान करते हैं, क्योंकि वे प्रोग्राम को मजबूर करते हैं कि वह या तो एरर को हैंडल करे या समाप्त हो जाए। वे एरर पहचानने की जगह (लाइब्रेरी कोड में गहराई में) को एरर संभालने की जगह (एप्लिकेशन कोड में) से अलग भी करते हैं, जो कई कॉल फ़्रेम दूर हो सकती है।
अधिकांश आधुनिक कंपाइलर पर C++ एक्सेप्शन “सफल पथ पर ज़ीरो-कॉस्ट” मॉडल का पालन करते हैं (टेबल-आधारित अनवाइंडिंग का उपयोग करके)। जब कोई एक्सेप्शन थ्रो नहीं होता, तब कोई रनटाइम ओवरहेड नहीं होता — कोई छिपी if जाँच नहीं, कोई ब्रांच प्रिडिक्शन मिस नहीं। इसकी कीमत केवल तब चुकानी पड़ती है जब एक्सेप्शन वास्तव में थ्रो होता है, इसीलिए एक्सेप्शन असाधारण (दुर्लभ) एरर स्थितियों के लिए आदर्श हैं।
throw, try और catch
मूल मैकेनिज़्म सरल है: एरर बताने के लिए किसी ऑब्जेक्ट को throw करें, जोखिम भरे कोड को try ब्लॉक में रखें, और मेल खाने वाले एक्सेप्शन टाइप को catch करें।
#include <iostream>
#include <stdexcept>
#include <string>
double divide(double numerator, double denominator) {
if (denominator == 0.0) {
throw std::invalid_argument("denominator cannot be zero");
}
return numerator / denominator;
}
int main() {
try {
double result = divide(10.0, 0.0);
std::cout << "Result: " << result << '\n';
} catch (const std::invalid_argument& e) {
std::cerr << "Invalid argument: " << e.what() << '\n';
} catch (const std::exception& e) {
std::cerr << "Error: " << e.what() << '\n';
} catch (...) {
std::cerr << "Unknown error occurred\n";
}
}स्टैक अनवाइंडिंग विस्तार से
जब कोई एक्सेप्शन थ्रो होता है, तो रनटाइम स्टैक अनवाइंडिंग करता है: वह कॉल स्टैक में पीछे की ओर चलता है और हर फ़्रेम के सभी लोकल वेरिएबल को उनके डिस्ट्रक्टर कॉल करके नष्ट करता है। इसीलिए RAII (Resource Acquisition Is Initialization) इतना महत्वपूर्ण है — यदि आप रिसोर्स को RAII रैपर में रखते हैं, तो अनवाइंडिंग के दौरान वे अपने-आप रिलीज़ हो जाते हैं।
अनवाइंडिंग की प्रक्रिया इस प्रकार काम करती है:
1. throw एक्सप्रेशन एक्सेप्शन ऑब्जेक्ट बनाता है (आमतौर पर मेमोरी के एक विशेष क्षेत्र में, स्टैक पर नहीं)।
2. रनटाइम वर्तमान फ़ंक्शन में मेल खाने वाला catch हैंडलर खोजता है।
3. यदि कोई नहीं मिलता, तो वह वर्तमान फ़ंक्शन से बाहर निकलता है और सभी लोकल ऑब्जेक्ट को उनके निर्माण के उल्टे क्रम में नष्ट करता है।
4. वह कॉल स्टैक में ऊपर की ओर चरण 2-3 दोहराता है।
5. यदि किसी भी फ़्रेम में हैंडलर नहीं मिलता, तो std::terminate() कॉल होता है।
मुख्य नियम: throw बाय वैल्यू करें, catch const रेफ़रेंस से करें। बाय वैल्यू थ्रो करने से डैंगलिंग पॉइंटर से बचाव होता है। const रेफ़रेंस से catch करने से स्लाइसिंग से बचाव होता है (जहाँ डिराइव्ड एक्सेप्शन बेस क्लास ऑब्जेक्ट में कॉपी हो जाता है और डिराइव्ड डेटा खो देता है) और अनावश्यक कॉपी भी नहीं बनतीं।
स्टैंडर्ड एक्सेप्शन हायरार्की
स्टैंडर्ड लाइब्रेरी std::exception से शुरू होने वाली एक हायरार्की देती है। इसे समझने से आप सही टाइप थ्रो कर पाते हैं और सही स्तर पर catch कर पाते हैं।
// Standard exception hierarchy (simplified)
//
// std::exception
// ├── std::logic_error (programmer mistakes - precondition violations)
// │ ├── std::invalid_argument
// │ ├── std::domain_error
// │ ├── std::length_error
// │ └── std::out_of_range
// ├── std::runtime_error (errors detectable only at runtime)
// │ ├── std::range_error
// │ ├── std::overflow_error
// │ ├── std::underflow_error
// │ └── std::system_error (OS-level errors with error_code)
// └── std::bad_alloc (memory allocation failure)
// std::bad_cast (failed dynamic_cast)
// std::bad_typeid (typeid on null pointer)
#include <stdexcept>
#include <vector>
#include <iostream>
int main() {
std::vector<int> v = {1, 2, 3};
try {
// .at() throws std::out_of_range, unlike operator[] which is UB
int val = v.at(10);
} catch (const std::out_of_range& e) {
std::cerr << "Out of range: " << e.what() << '\n';
}
}कस्टम एक्सेप्शन क्लास
डोमेन-विशिष्ट एक्सेप्शन बनाने के लिए std::exception या उसकी किसी सबक्लास से डिराइव करें। सार्थक संदेश देने के लिए what() को हमेशा ओवरराइड करें।
#include <stdexcept>
#include <string>
#include <iostream>
class FileParseError : public std::runtime_error {
public:
FileParseError(const std::string& filename, int line, const std::string& msg)
: std::runtime_error(
"Parse error in '" + filename + "' at line " +
std::to_string(line) + ": " + msg),
filename_(filename),
line_(line) {}
const std::string& filename() const noexcept { return filename_; }
int line() const noexcept { return line_; }
private:
std::string filename_;
int line_;
};
void parse_config(const std::string& filename) {
// Simulate a parse failure at line 42
throw FileParseError(filename, 42, "unexpected token '@@'");
}
int main() {
try {
parse_config("app.conf");
} catch (const FileParseError& e) {
std::cerr << e.what() << '\n';
std::cerr << " File: " << e.filename()
<< ", Line: " << e.line() << '\n';
}
}noexcept और एक्सेप्शन स्पेसिफ़िकेशन
noexcept स्पेसिफ़ायर कंपाइलर को (और पढ़ने वालों को) बताता है कि फ़ंक्शन कभी कोई एक्सेप्शन थ्रो नहीं करेगा। यदि noexcept फ़ंक्शन फिर भी थ्रो करता है, तो तुरंत std::terminate() कॉल होता है — कोई स्टैक अनवाइंडिंग नहीं होती।
noexcept क्यों मायने रखता है?
- परफ़ॉर्मेंस: कंपाइलर noexcept फ़ंक्शन के लिए अनवाइंडिंग टेबल छोड़ सकता है, जिससे कोड अधिक कसा हुआ बनता है।
- STL ऑप्टिमाइज़ेशन: std::vector जैसे कंटेनर रीअलोकेशन के दौरान एलिमेंट को केवल तभी मूव करते हैं जब मूव कंस्ट्रक्टर noexcept हो। अन्यथा, स्ट्रॉन्ग एक्सेप्शन गारंटी बनाए रखने के लिए वे कॉपी करने पर लौट आते हैं। इससे परफ़ॉर्मेंस में भारी अंतर पड़ सकता है।
- डॉक्यूमेंटेशन: यह कॉलर के साथ एक कॉन्ट्रैक्ट है कि फ़ंक्शन विफल नहीं हो सकता।
noexcept का उपयोग इन पर करें: मूव कंस्ट्रक्टर, मूव असाइनमेंट ऑपरेटर, swap फ़ंक्शन, डिस्ट्रक्टर (जो अप्रत्यक्ष रूप से noexcept होते हैं), सरल गेटर, और तुलना ऑपरेटर।
पुराना throw() स्पेसिफ़िकेशन (और throw(type-list)) C++11 में डिप्रिकेट हुआ। type-list वाला रूप C++17 में हटा दिया गया, जबकि खाली कोष्ठक वाला रूप C++20 में हटा दिया गया। इसके बजाय हमेशा noexcept का उपयोग करें।
डिस्ट्रक्टर में कभी एक्सेप्शन थ्रो न करें। किसी दूसरे एक्सेप्शन से हो रही स्टैक अनवाइंडिंग के दौरान दूसरा एक्सेप्शन थ्रो करने पर std::terminate() कॉल हो जाता है। यदि डिस्ट्रक्टर को कोई ऐसा ऑपरेशन करना ही पड़े जो विफल हो सकता है, तो एक्सेप्शन को डिस्ट्रक्टर के अंदर ही catch करें और वहीं संभालें (जैसे, लॉग करें और दबा दें)।
- बाय वैल्यू catch करने से डिराइव्ड एक्सेप्शन स्लाइस हो जाते हैं — हमेशा
const&से catch करें - पॉइंटर थ्रो करने (जैसे
throw new std::runtime_error(...)) से मेमोरी लीक होती है — बाय वैल्यू थ्रो करें ...को पहले catch करने से उसके नीचे के सभी विशिष्ट हैंडलर छिप जाते हैं — इसे सबसे अंत में रखें- स्टैक अनवाइंडिंग के दौरान डिस्ट्रक्टर में एक्सेप्शन
std::terminate()का कारण बनते हैं - कंट्रोल फ़्लो के लिए एक्सेप्शन का उपयोग (गैर-एरर पथों में) ज़ीरो-कॉस्ट मॉडल को बेअसर कर देता है और परफ़ॉर्मेंस को नुकसान पहुँचाता है
- स्लाइसिंग और लीक से बचने के लिए बाय वैल्यू throw करें और
constरेफ़रेंस से catch करें - स्टैक अनवाइंडिंग लोकल ऑब्जेक्ट को निर्माण के उल्टे क्रम में नष्ट करती है — RAII क्लीनअप सुनिश्चित करता है
- C++ एक्सेप्शन सफल पथ पर ज़ीरो-कॉस्ट हैं (टेबल-आधारित अनवाइंडिंग), केवल थ्रो होने पर महँगे पड़ते हैं
noexceptकंपाइलर ऑप्टिमाइज़ेशन सक्षम करता है और STL द्वारा उपयोग किए जाने वाले मूव ऑपरेशन के लिए अत्यंत महत्वपूर्ण है- कस्टम एक्सेप्शन को
std::runtime_errorयाstd::logic_errorसे डिराइव करें औरwhat()को ओवरराइड करें
क्विज़ — अपना ज्ञान परखें
(15 XP)1. आपको एक्सेप्शन को बाय वैल्यू के बजाय `const&` से क्यों catch करना चाहिए?
2. यदि `noexcept` चिह्नित फ़ंक्शन कोई एक्सेप्शन थ्रो करे तो क्या होता है?
3. स्टैक अनवाइंडिंग के दौरान हर स्टैक फ़्रेम के लोकल वेरिएबल का क्या होता है?