استراتيجيات معالجة الأخطاء: اختيار النهج الصحيح
قارن بين الاستثناءات وأكواد الأخطاء وstd::expected - تعرف على متى يكون كل نهج مناسبًا وكيفية التعامل مع الأخطاء في المنشئات والمدمرات والتعليمات البرمجية متعددة الخيوط.
لا يوجد حل واحد يناسب الجميع
تقدم C++ آليات متعددة لمعالجة الأخطاء، ولكل منها مقايضات مختلفة. يعتمد اختيار الخيار المناسب على المجال الخاص بك:
الاستثناءات هي الأفضل للأخطاء النادرة و التي لا يمكن معالجتها محليًا. ليس لها تكلفة على مسار النجاح ولكنها باهظة الثمن عند طرحها (1000 دولار من النانو ثانية). يتم نشرها تلقائيًا عبر إطارات الاتصال.
رموز الخطأ (قيم الإرجاع،errno،std::error_code) هي الأفضل للأخطاء المتوقعة والمتكررة. لديهم تكلفة صغيرة وثابتة على كل مكالمة (يجب على المتصل التحقق من قيمة الإرجاع). إنها المعيار في واجهات برمجة تطبيقات C والتعليمات البرمجية على مستوى kernel.
يجمع std::expected (C++23) بين أفضل ما في الاثنين: فهو يجبر المتصل على الاعتراف بالخطأ (مثل رموز الخطأ) ولكنه يحمل نوعًا من الأخطاء الغنية (مثل الاستثناءات) ويدعم التسلسل الأحادي. لقد أصبح بسرعة النهج المفضل في كود C++ الجديد.
الفكرة الأساسية: هذه ليست فلسفات متنافسة، بل هي أدوات لمواقف مختلفة، وغالبًا ما يستخدم رمز الإنتاج الثلاثة.
المقارنة: الاستثناءات مقابل رموز الأخطاء مقابل std::expected
| المعيار | استثناءات | رموز الخطأ |std::expected|
|---|---|---|---|
| التكلفة على النجاح | صفر (معتمد على الجدول) | فرع + شيك | الحد الأدنى (قيمة المخازن) |
| التكلفة على الخطأ | عالية (الاسترخاء) | نفس النجاح | نفس النجاح |
| هل يمكن تجاهله؟ | لا (ينتهي البرنامج) | نعم (البق الصامت) | أصعب (يجب الوصول إلى.value()) |
| البناة | الخيار الوحيد | لا يمكن إرجاع قيمة | لا يمكن إرجاع قيمة |
| المدمرات | يجب ألا يرمي | غرامة | غرامة |
| القابلية للتركيب | محدود (محاولة/قبض) | سلاسل if اليدوية | موناديك (و_ثم، تحويل) |
| الحجم الثنائي | أكبر (طاولات الاسترخاء) | الحد الأدنى | الحد الأدنى |
| آمن في الوقت الحقيقي؟ | لا (زمن غير محدود) | نعم | نعم |
معالجة الأخطاء في المنشئين
لا يمكن للمنشئين إرجاع رموز الخطأ - ليس لديهم قيمة إرجاع. الاستثناءات هي الآلية النظيفة الوحيدة للإشارة إلى فشل البناء. الاستثناء المطروح يعني أن الكائن لم يتم إنشاؤه بالكامل مطلقًا، لذلك لا يتم استدعاء المدمر الخاص به أبدًا (ولكن يتم استدعاء المدمرات للكائنات الفرعية والقواعد التي تم إنشاؤها بالفعل).
#include <fstream>
#include <string>
#include <stdexcept>
#include <iostream>
class ConfigFile {
public:
explicit ConfigFile(const std::string& path)
: path_(path), stream_(path) {
if (!stream_.is_open()) {
throw std::runtime_error(
"Cannot open config file: " + path);
}
// Parse the file — if this throws, stream_ is
// already constructed and its destructor will
// close the file automatically (RAII)
parse();
}
std::string get(const std::string& key) const {
auto it = data_.find(key);
if (it == data_.end()) {
throw std::out_of_range("Key not found: " + key);
}
return it->second;
}
private:
void parse() {
std::string line;
while (std::getline(stream_, line)) {
auto pos = line.find('=');
if (pos != std::string::npos) {
data_[line.substr(0, pos)] = line.substr(pos + 1);
}
}
}
std::string path_;
std::ifstream stream_; // RAII: closed by destructor
std::map<std::string, std::string> data_;
};
// Usage: the object is fully valid or does not exist
int main() {
try {
ConfigFile cfg("/etc/myapp.conf");
std::cout << cfg.get("port") << '\n';
} catch (const std::exception& e) {
std::cerr << e.what() << '\n';
}
}std::error_code للأخطاء على مستوى النظام
يوفرstd::error_codeآلية خفيفة الوزن وغير قابلة للرمي للأخطاء على مستوى النظام. تعد مكتبةمثالًا رائعًا - تحتوي معظم الوظائف على تحميل زائد يأخذ معلمةstd::error_code&بدلاً من الرمي.
#include <filesystem>
#include <iostream>
#include <system_error>
namespace fs = std::filesystem;
// Throwing version — suitable for unexpected errors
void copy_file_throwing(const fs::path& src, const fs::path& dst) {
fs::copy_file(src, dst); // throws fs::filesystem_error on failure
}
// Non-throwing version — suitable for expected failures
bool copy_file_safe(const fs::path& src, const fs::path& dst) {
std::error_code ec;
fs::copy_file(src, dst, ec); // sets ec instead of throwing
if (ec) {
std::cerr << "Copy failed: " << ec.message()
<< " (" << ec.category().name()
<< ':' << ec.value() << ")\n";
return false;
}
return true;
}
int main() {
// The error_code version is preferable in loops where
// failure is expected (e.g., batch file processing)
for (const auto& entry : fs::directory_iterator(".")) {
std::error_code ec;
auto sz = fs::file_size(entry.path(), ec);
if (!ec) {
std::cout << entry.path().filename()
<< ": " << sz << " bytes\n";
}
}
}المدمرات في C++11 والإصدارات الأحدث هي ضمنيًاnoexcept. إذا رميت أداة التدمير، يتم استدعاءstd::terminate(). حتى إذا قمت بوضع علامةnoexcept(false)على أداة التدمير بشكل صريح، فإن الرمي أثناء تفكيك المكدس (عندما يكون هناك استثناء آخر نشط بالفعل) يستدعيstd::terminate().
- التسجيل والمنع: في حالة فشل عملية التنظيف، قم بتسجيل الخطأ ثم تابع
- توفير طريقة
close()منفصلة : اسمح للمتصلين بمعالجة أخطاء التنظيف بشكل صريح قبل التدمير - تصميم الموارد بحيث لا يمكن أن تفشل عملية التنظيف: على سبيل المثال، ينجح إغلاق واصف الملف دائمًا على مستوى نظام التشغيل
- لا تستدعي مطلقًا وظيفة رمي من أداة التدمير دون تغليفها بمحاولة/التقاط
معالجة الأخطاء في التعليمات البرمجية متعددة مؤشرات الترابط
الاستثناءات لا تعبر حدود الخيط. إذا تم طرح مؤشر ترابط ولم يتم اكتشاف الاستثناء داخل مؤشر الترابط هذا، فسيتم استدعاءstd::terminate(). لنشر الاستثناءات بين سلاسل الرسائل:
1. استخدم std::future و std::async: يتم التقاط الاستثناءات التي تم طرحها في المهمة غير المتزامنة وإعادة طرحها عند الاتصال بـ.get()في المستقبل.
2. استخدم std::exception_ptr معstd::current_exception()وstd::rethrow_exception()لالتقاط الاستثناءات ونقلها يدويًا.
3. استخدم std::promise::set_exception() لإرسال استثناء من سلسلة رسائل المنتج إلى المستهلك.
بالنسبة للتعليمات البرمجية المتزامنة عالية الإنتاجية، غالبًا ما يتم تفضيل رموز الخطأ أوstd::expectedلأنها تتجنب تكلفة البنية التحتية الاستثناءية في كل مؤشر ترابط كما أنها أسهل في التفكير عبر حدود الخيط.
مطابقة آلية معالجة الأخطاء مع الموقف.
- استخدم الاستثناءات للأخطاء النادرة، التي لا يمكن معالجتها محليًا، والتي يجب أن تنتشر لأعلى المكدس (على سبيل المثال، لم يتم العثور على الملف، قاعدة البيانات معطلة)
- استخدم رموز الخطأ للعمليات عالية التردد حيث يكون الفشل طبيعيًا ومتوقعًا (على سبيل المثال، فشل البحث في ذاكرة التخزين المؤقت، أو حظر الإدخال/الإخراج غير المحظور)
- استخدم
std::expectedللحصول على تعليمات برمجية جديدة حيث تريد معالجة الأخطاء بشكل صريح مع أنواع الأخطاء الغنية والتركيب الأحادي - استخدم الاستثناءات في المُنشئات — فهي الطريقة الوحيدة للإشارة إلى الفشل قبل وجود الكائن
- لا تستخدم أدوات التدمير مطلقًا — قم بتسجيل الخطأ وقم بقمعه
- الاستثناءات ورموز الأخطاء و
std::expectedهي أدوات تكميلية - وليست فلسفات متنافسة - يجب على المنشئين استخدام الاستثناءات للإشارة إلى الفشل؛ رموز الخطأ ليست خيارا
- يجب على المدمرات عدم الرمي مطلقًا — فهي
noexceptضمنيًا منذ C++11 - الاستثناءات لا تعبر حدود الخيط — استخدم
std::futureأوstd::exception_ptrلنقلها - يعد
std::error_codeمثاليًا للأخطاء على مستوى النظام حيث تريد واجهة برمجة تطبيقات غير قابلة للرمي
اختبار — اختبر معرفتك
(15 نقطة خبرة)1. لماذا تعتبر الاستثناءات هي الآلية المفضلة لمعالجة الأخطاء في المنشئين؟
2. ماذا يحدث إذا تم طرح استثناء في أداة التدمير أثناء تفكيك المكدس من استثناء آخر؟
3. كيف يمكن نقل الاستثناءات بين المواضيع؟