Exception Handling — C++ المتقدم
معالجة الاستثناءات (Exceptions)
الدالة التي تكتشف خطأ لا تستطيع إصلاحه (ملف غير موجود، مدخل غير صالح، ذاكرة نفدت) تحتاج طريقة لإبلاغ من يستطيع. الاستثناء يقفز من مكان الخطأ إلى أقرب catch مناسب، ويدمّر في الطريق كل الكائنات المحلية (هنا تظهر قيمة RAII).
الأساسيات
#include <iostream>
#include <stdexcept>
using namespace std;
double divide(double a, double b) {
if (b == 0) throw invalid_argument("القسمة على صفر");
return a / b;
}
int main() {
try {
cout << divide(10, 2) << '\n'; // 5
cout << divide(1, 0) << '\n'; // يرمي — هذا السطر لا يكمل
cout << "لن يُطبع\n";
} catch (const invalid_argument& e) {
cout << "خطأ: " << e.what() << '\n';
}
cout << "البرنامج مستمر\n";
}التقط دائماً بـ const&: الالتقاط بالقيمة ينسخ ويقطّع (slicing) الاستثناءات المشتقة.
التسلسل القياسي
exception
├── logic_error (خطأ في منطق البرنامج — كان يمكن تجنبه)
│ ├── invalid_argument
│ ├── out_of_range ← vector::at و string::at
│ └── length_error
├── runtime_error (خطأ لا يُعرف إلا وقت التشغيل)
│ └── overflow_error ...
└── bad_alloc ← فشل newترتيب catch مهم: من الأخص إلى الأعم. إذا وضعت catch (const exception&) أولاً فستلتقط كل شيء ولن تصل للأخص.
استثناء مخصص
اشتق من نوع قياسي مناسب وأضف معلومات:
#include <stdexcept>
#include <string>
using namespace std;
class InsufficientFunds : public runtime_error {
public:
double needed;
explicit InsufficientFunds(double n)
: runtime_error("رصيد غير كافٍ، ينقص " + to_string(n)), needed(n) {}
};
// throw InsufficientFunds(25.5);إعادة الرمي و catch(...)
#include <iostream>
#include <stdexcept>
using namespace std;
void step() { throw runtime_error("فشل الاتصال"); }
int main() {
try {
try {
step();
} catch (const exception& e) {
cout << "تسجيل: " << e.what() << '\n';
throw; // يعيد رمي نفس الكائن الأصلي (لا تكتب throw e;)
}
} catch (...) { // يلتقط أي شيء — للمستوى الأعلى فقط
cout << "خطأ عام\n";
}
}ضمانات الأمان (Exception Safety)
عند كتابة دالة اسأل: إذا رُمي استثناء في منتصفها، في أي حالة يبقى الكائن؟
| الضمان | المعنى |
|---|---|
| لا رمي (nothrow) | لا ترمي أبداً — المدمّرات، swap، دوال النقل |
| قوي (strong) | إما تنجح كلياً أو لا يتغير شيء (مثل copy-and-swap) |
| أساسي (basic) | لا تسرب، والكائن صالح لكن قد تغيرت قيمته |
RAII يمنحك الضمان الأساسي تقريباً مجاناً: لا new و delete يدويين = لا تسرب عند القفز.
noexcept
void f() noexcept وعد بأن الدالة لا ترمي. إذا رمت رغم ذلك يُستدعى std::terminate وينتهي البرنامج. أهم استخدام: مُنشئ النقل — vector لا ينقل العناصر عند التوسّع إلا إذا كان النقل noexcept، وإلا ينسخها ليحافظ على الضمان القوي.
متى لا تستخدم الاستثناءات؟
- للحالات المتوقعة في التدفق الطبيعي (المستخدم لم يجد نتيجة بحث): أعد optional أو bool (درس أدوات C++ الحديثة).
- لا ترمِ من المدمّر أبداً.
- الاستثناءات للأخطاء الاستثنائية التي لا تستطيع الدالة الحالية التعامل معها.
تمرين
اكتب int parsePositive(const string& s) تستخدم stoi (التي ترمي invalid_argument و out_of_range) وترمي invalid_argument خاصاً بك إذا كان العدد ≤ 0. في main التقط كل نوع برسالة مختلفة، وجرّب: "42"، "abc"، "-5"، "99999999999".
#include <iostream>
#include <stdexcept>
#include <vector>
#include <string>
using namespace std;
// استثناء مخصص
class InsufficientFundsException : public exception {
private:
double amount;
double balance;
string message;
public:
InsufficientFundsException(double amt, double bal)
: amount(amt), balance(bal) {
message = "رصيد غير كافٍ! المطلوب: " + to_string(amt)
+ ", المتاح: " + to_string(bal);
}
const char* what() const noexcept override {
return message.c_str();
}
double getShortfall() const { return amount - balance; }
};
class BankAccount {
private:
double balance;
public:
BankAccount(double initial) : balance(initial) {
if (initial < 0) {
throw invalid_argument("الرصيد لا يمكن أن يكون سالباً");
}
}
void deposit(double amount) {
if (amount <= 0) {
throw invalid_argument("مبلغ الإيداع يجب أن يكون موجباً");
}
balance += amount;
}
void withdraw(double amount) {
if (amount <= 0) {
throw invalid_argument("مبلغ السحب يجب أن يكون موجباً");
}
if (amount > balance) {
throw InsufficientFundsException(amount, balance);
}
balance -= amount;
}
double getBalance() const noexcept { return balance; }
};
// دالة مع عدة استثناءات
double divide(double a, double b) {
if (b == 0) {
throw runtime_error("قسمة على صفر!");
}
return a / b;
}
int getElement(const vector<int>& vec, int index) {
if (index < 0 || index >= vec.size()) {
throw out_of_range("فهرس خارج النطاق: " + to_string(index));
}
return vec[index];
}
int main() {
// ===== try-catch أساسي =====
cout << "=== try-catch أساسي ===" << endl;
try {
double result = divide(10, 0);
cout << "النتيجة: " << result << endl;
} catch (const runtime_error& e) {
cout << "❌ خطأ: " << e.what() << endl;
}
// ===== عدة catch blocks =====
cout << "\n=== عدة catch ===" << endl;
vector<int> nums = {1, 2, 3, 4, 5};
try {
cout << getElement(nums, 10) << endl;
} catch (const out_of_range& e) {
cout << "❌ خطأ في النطاق: " << e.what() << endl;
} catch (const exception& e) {
cout << "❌ خطأ عام: " << e.what() << endl;
}
// ===== استثناء مخصص =====
cout << "\n=== استثناء مخصص ===" << endl;
try {
BankAccount account(1000);
account.withdraw(1500);
} catch (const InsufficientFundsException& e) {
cout << "❌ " << e.what() << endl;
cout << " ينقصك: $" << e.getShortfall() << endl;
} catch (const invalid_argument& e) {
cout << "❌ خطأ في الإدخال: " << e.what() << endl;
}
// ===== catch all =====
cout << "\n=== catch all ===" << endl;
try {
throw 42; // رمي أي شيء
} catch (...) {
cout << "❌ حدث خطأ غير معروف!" << endl;
}
// ===== إعادة الرمي =====
cout << "\n=== إعادة الرمي ===" << endl;
try {
try {
throw runtime_error("خطأ داخلي");
} catch (const exception& e) {
cout << "معالجة أولية: " << e.what() << endl;
throw; // إعادة الرمي
}
} catch (const exception& e) {
cout << "معالجة نهائية: " << e.what() << endl;
}
// ===== noexcept =====
cout << "\n=== noexcept ===" << endl;
BankAccount account(500);
cout << "الرصيد: " << account.getBalance() << endl; // آمن دائماً
cout << "\n✓ البرنامج استمر بنجاح!" << endl;
return 0;
}اختبر فهمك