एक्सेप्शन सेफ़्टी गारंटी
एक्सेप्शन सेफ़्टी के तीन स्तर सीखें — बेसिक, स्ट्रॉन्ग और नो-थ्रो — और जानें कि RAII और कॉपी-एंड-स्वैप इन्हें कैसे प्रदान करते हैं।
तीन गारंटी
एक्सेप्शन सेफ़्टी का मतलब एक्सेप्शन को रोकना नहीं है — यह इस बारे में है कि जब कोई एक्सेप्शन आता है तब आपका कोड क्या गारंटी देता है। आपके द्वारा लिखा गया हर फ़ंक्शन अप्रत्यक्ष रूप से इनमें से कोई एक स्तर प्रदान करता है:
1. नो-थ्रो गारंटी (सबसे मज़बूत): फ़ंक्शन कभी थ्रो नहीं करता। यह हमेशा सफल होता है। उदाहरण: डिस्ट्रक्टर, swap, सरल गेटर। इन्हें noexcept चिह्नित किया जाता है।
2. स्ट्रॉन्ग गारंटी (कमिट-या-रोलबैक): यदि फ़ंक्शन थ्रो करता है, तो प्रोग्राम की स्थिति ठीक वैसी ही रहती है जैसी कॉल से पहले थी। मानो फ़ंक्शन कभी कॉल ही नहीं हुआ। इसे ऐसे डेटाबेस ट्रांज़ैक्शन की तरह समझें जो या तो पूरी तरह कमिट होता है या पूरी तरह रोलबैक।
3. बेसिक गारंटी (न्यूनतम स्वीकार्य): यदि फ़ंक्शन थ्रो करता है, तो प्रोग्राम वैध लेकिन अनिर्दिष्ट स्थिति में होता है। कोई रिसोर्स लीक नहीं होता, सभी इन्वेरिएंट बने रहते हैं, लेकिन सटीक स्थिति वैसी नहीं भी हो सकती जैसी पहले थी।
एक चौथा, अस्वीकार्य स्तर भी है: कोई गारंटी नहीं — जहाँ एक्सेप्शन प्रोग्राम को लीक हुए रिसोर्स के साथ भ्रष्ट स्थिति में छोड़ सकता है। प्रोडक्शन कोड को कभी भी बेसिक गारंटी से कम नहीं देना चाहिए।
RAII: एक्सेप्शन सेफ़्टी की नींव
RAII (Resource Acquisition Is Initialization) किसी रिसोर्स के जीवनकाल को किसी ऑब्जेक्ट के जीवनकाल से बाँधता है। कंस्ट्रक्टर रिसोर्स प्राप्त करता है; डिस्ट्रक्टर उसे रिलीज़ करता है। चूँकि डिस्ट्रक्टर स्टैक अनवाइंडिंग के दौरान चलते हैं, RAII अपने-आप बेसिक गारंटी प्रदान करता है।
#include <fstream>
#include <string>
#include <stdexcept>
#include <iostream>
class DatabaseConnection {
public:
explicit DatabaseConnection(const std::string& conn_str)
: conn_str_(conn_str), connected_(false) {
// Simulate connection
if (conn_str.empty()) {
throw std::invalid_argument("empty connection string");
}
connected_ = true;
std::cout << "Connected to: " << conn_str_ << '\n';
}
~DatabaseConnection() {
if (connected_) {
// Cleanup never throws
std::cout << "Disconnected from: " << conn_str_ << '\n';
connected_ = false;
}
}
void execute(const std::string& query) {
if (!connected_) throw std::runtime_error("not connected");
// Even if this function throws, the destructor
// will still close the connection during unwinding
std::cout << "Executing: " << query << '\n';
}
// Non-copyable, movable
DatabaseConnection(const DatabaseConnection&) = delete;
DatabaseConnection& operator=(const DatabaseConnection&) = delete;
DatabaseConnection(DatabaseConnection&& other) noexcept
: conn_str_(std::move(other.conn_str_)),
connected_(other.connected_) {
other.connected_ = false;
}
private:
std::string conn_str_;
bool connected_;
};
void process_data() {
DatabaseConnection db("postgres://localhost/mydb");
db.execute("SELECT * FROM users");
// If execute() throws, ~DatabaseConnection() still runs
// — the connection is always properly closed
}स्ट्रॉन्ग गारंटी के लिए कॉपी-एंड-स्वैप
कॉपी-एंड-स्वैप आइडिओम असाइनमेंट ऑपरेटर के लिए स्ट्रॉन्ग एक्सेप्शन गारंटी प्रदान करता है। विचार यह है: पहले एक कॉपी बनाएँ (जो थ्रो कर सकती है), फिर कॉपी के साथ स्वैप करें (जो कभी थ्रो नहीं करता)। यदि कॉपी थ्रो करती है, तो मूल ऑब्जेक्ट अछूता रहता है।
#include <algorithm>
#include <utility>
#include <cstddef>
class Matrix {
public:
Matrix(std::size_t rows, std::size_t cols)
: rows_(rows), cols_(cols),
data_(new double[rows * cols]{}) {}
// Copy constructor — may throw (allocates memory)
Matrix(const Matrix& other)
: rows_(other.rows_), cols_(other.cols_),
data_(new double[other.rows_ * other.cols_]) {
std::copy(other.data_,
other.data_ + rows_ * cols_,
data_);
}
// Move constructor — never throws
Matrix(Matrix&& other) noexcept
: rows_(other.rows_), cols_(other.cols_),
data_(other.data_) {
other.data_ = nullptr;
other.rows_ = other.cols_ = 0;
}
~Matrix() { delete[] data_; }
// No-throw swap — the key ingredient
friend void swap(Matrix& a, Matrix& b) noexcept {
using std::swap;
swap(a.rows_, b.rows_);
swap(a.cols_, b.cols_);
swap(a.data_, b.data_);
}
// Copy-and-swap assignment: STRONG guarantee
// Takes parameter by value (invokes copy/move ctor)
Matrix& operator=(Matrix other) noexcept {
swap(*this, other);
return *this;
// 'other' is destroyed here, freeing old data
}
private:
std::size_t rows_, cols_;
double* data_;
};noexcept और मूव ऑपरेशन
STL मूव कंस्ट्रक्टर के लिए noexcept पर बहुत अधिक निर्भर करती है। std::vector::push_back पर विचार करें: जब वेक्टर को रीअलोकेट करना पड़ता है, तो उसे पुराने बफ़र से एलिमेंट नए बफ़र में ट्रांसफ़र करने होते हैं।
- यदि एलिमेंट का मूव कंस्ट्रक्टर noexcept है, तो वेक्टर एलिमेंट को मूव करता है (तेज़)।
- यदि मूव कंस्ट्रक्टर थ्रो कर सकता है, तो वेक्टर इसके बजाय एलिमेंट को कॉपी करता है (धीमा, लेकिन सुरक्षित — यदि कोई कॉपी थ्रो करती है, तो मूल बफ़र अब भी सुरक्षित रहता है)।
इसीलिए मूव कंस्ट्रक्टर को noexcept चिह्नित करना केवल अच्छी आदत नहीं है — कंटेनर में रखे जाने वाले टाइप के लिए यह 10 गुना या 100 गुना परफ़ॉर्मेंस का अंतर पैदा कर सकता है।
आप कंपाइल टाइम पर std::is_nothrow_move_constructible_v से जाँच कर सकते हैं। STL अपनी रणनीति तय करने के लिए आंतरिक रूप से इसी ट्रेट का उपयोग करती है।
गारंटी का स्तर पहचानना
फ़ंक्शन लिखते समय सोचें कि वह कौन-सी गारंटी देता है। यहाँ एक ही क्लास में तीनों स्तरों का उदाहरण है।
#include <vector>
#include <string>
#include <algorithm>
#include <stdexcept>
class UserRegistry {
public:
// NO-THROW: simple getter, can never fail
std::size_t size() const noexcept {
return users_.size();
}
// STRONG GUARANTEE: either the user is added or nothing changes
void add_user(const std::string& name) {
if (name.empty()) {
throw std::invalid_argument("name cannot be empty");
}
// If push_back throws (e.g., bad_alloc during reallocation),
// vector guarantees the original data is intact.
users_.push_back(name);
}
// BASIC GUARANTEE: if an exception occurs partway through,
// some users may have been processed and some may not.
// But the object is still in a valid state (no leaks, no corruption).
void normalize_all() {
for (auto& user : users_) {
// std::transform could theoretically throw
// (e.g., if user string allocation fails during tolower)
std::transform(user.begin(), user.end(), user.begin(),
[](unsigned char c) { return std::tolower(c); });
}
}
private:
std::vector<std::string> users_;
};एक्सेप्शन-सेफ़ कोड को लगातार लिखने के लिए इन दिशानिर्देशों का पालन करें।
- हर जगह RAII का उपयोग करें: कच्चे रिसोर्स कभी न रखें — उन्हें स्मार्ट पॉइंटर या RAII रैपर में लपेटें
- मूव कंस्ट्रक्टर और swap को
noexceptचिह्नित करें: इससे STL ऑप्टिमाइज़ेशन सक्षम होते हैं और नो-थ्रो गारंटी मिलती है - स्थिति बदलने से पहले वह सारा काम कर लें जो थ्रो कर सकता है: पहले अलोकेट, गणना और वैलिडेशन करें — फिर
swapजैसे नो-थ्रो ऑपरेशन से कमिट करें - स्ट्रॉन्ग गारंटी न्यूनतम प्रयास में पाने के लिए असाइनमेंट ऑपरेटर में कॉपी-एंड-स्वैप आइडिओम को प्राथमिकता दें
- डिस्ट्रक्टर को कभी थ्रो नहीं करना चाहिए: C++11 और बाद के संस्करणों में वे अप्रत्यक्ष रूप से
noexceptहोते हैं
ये पैटर्न एक्सेप्शन सेफ़्टी गारंटी को तोड़ देते हैं और सूक्ष्म बग पैदा करते हैं।
- सभी थ्रो कर सकने वाले ऑपरेशन पूरे होने से पहले स्थिति बदलना: यदि 3 में से दूसरा चरण थ्रो करता है, तो पहला चरण ऑब्जेक्ट को पहले ही बदल चुका होता है
- यह भूल जाना कि
newसेstd::bad_allocथ्रो हो सकता है: हर डायनेमिक अलोकेशन थ्रो का संभावित बिंदु है noexceptन होने वाले मूव कंस्ट्रक्टर: STL चुपचाप कॉपी करने पर लौट जाएगी, जिससे परफ़ॉर्मेंस बिगड़ जाएगा- एक्सेप्शन को बहुत व्यापक रूप से catch करना: दोबारा थ्रो किए बिना
catch(...)असली समस्या को छिपा देता है
- तीन गारंटी ये हैं: नो-थ्रो (हमेशा सफल), स्ट्रॉन्ग (कमिट-या-रोलबैक), और बेसिक (वैध लेकिन अनिर्दिष्ट स्थिति)
- RAII नींव है — अनवाइंडिंग के दौरान डिस्ट्रक्टर चलते हैं, जिससे रिसोर्स हमेशा मुक्त होते हैं
- कॉपी-एंड-स्वैप स्ट्रॉन्ग गारंटी देता है: पहले थ्रो कर सकने वाला सारा काम करें, फिर
swapकरें (जोnoexceptहै) noexceptमूव कंस्ट्रक्टर अत्यंत महत्वपूर्ण हैं — इनके बिनाstd::vectorरीअलोकेशन पर कॉपी करने लगता है- हर फ़ंक्शन को कम से कम बेसिक गारंटी का लक्ष्य रखना चाहिए; जहाँ संभव हो, नो-थ्रो
क्विज़ — अपना ज्ञान परखें
(20 XP)1. स्ट्रॉन्ग एक्सेप्शन गारंटी क्या वादा करती है?
2. मूव कंस्ट्रक्टर `noexcept` न होने पर `std::vector` एलिमेंट को मूव करने के बजाय कॉपी क्यों करता है?
3. कॉपी-एंड-स्वैप आइडिओम में असाइनमेंट ऑपरेटर अपना पैरामीटर बाय वैल्यू क्यों लेता है?