Menu

חריגות ב-C++: throw, what() וטיפול בשגיאות

חריגות מדווחות על שגיאות שפונקציה לא יכולה לטפל בהן במקום. למדו איך לבצע throw, מהם טיפוסי החריגות הסטנדרטיים, את הודעת what(), ולמה חריגות עדיפות על קודי החזרה בכישלונות שחשובים.

בדף הזה יש עורכים שאפשר להריץ - לערוך, להריץ ולראות את הפלט מיד.

למה יש חריגות

בעמוד הקודם השתמשתם ב-enum class כדי לתת שמות משמעותיים למצבי שגיאה. זה מצוין לתוצאות שפונקציה מצפה להן והקורא אמור לבדוק. אבל יש כישלונות מסוג אחר: פונקציה עמוק בערימת הקריאות מגלה שקובץ לא נפתח, או שארגומנט לא הגיוני, ואין לה מושג מה התוכנית צריכה לעשות עם זה. החזרת קוד שגיאה עובדת רק אם כל קורא בשרשרת זוכר לבדוק אותו ולהעביר אותו הלאה. פספסו בדיקה אחת והתוכנית ממשיכה להפליג עם זבל.

חריגות פותרות את זה. כשמשהו משתבש, זורקים אובייקט עם throw. הביצוע נעצר מיד, ה-stack נפרם (לכל אובייקט מקומי בין ה-throw למטפל מורץ ה-destructor), והשליטה קופצת ל-catch התואם הקרוב ביותר. אי אפשר להתעלם בשקט מחריגה שלא טופלה: אם שום דבר לא תופס אותה, התוכנית קוראת ל-std::terminate ונעצרת.

העמוד הזה מתמקד בצד הזורק, כלומר באובייקטי השגיאה עצמם. העמוד הבא צולל למנגנון של try/catch בפירוט.

זריקה והודעת what()

מבחינה טכנית אפשר לזרוק כל ערך, throw 42; או throw "oops"; חוקיים, אבל אל תעשו את זה. המוסכמה שכולם עוקבים אחריה היא לזרוק אובייקט שנגזר מ-std::exception. מחלקת הבסיס הזו מצהירה על מתודה וירטואלית אחת, what(), שמחזירה const char* שמתאר את הבעיה. היצמדות למוסכמה אומרת ש-catch (const std::exception& e) אחד יכול לטפל בכל דבר.

ה-header <stdexcept> נותן לכם טיפוסים מוכנים שהבנאי שלהם מקבל את ההודעה:

שימו לב ש-what() מחזירה בדיוק את המחרוזת שאיתה בניתם את החריגה. שימו לב גם שתפסנו לפי const exception& למרות שזרקנו runtime_error: זה עובד כי runtime_error הוא std::exception (קשר שתזהו מעמוד הירושה).

היררכיית החריגות הסטנדרטית

לפני שכותבים טיפוס חריגה משלכם, בדקו אם בספרייה הסטנדרטית כבר יש אחד שמתאים. כולם יורשים מ-std::exception ומתחלקים לשתי משפחות ב-<stdexcept>:

  • logic_error: באג בלוגיקה של התוכנית שעקרונית אפשר היה לתפוס לפני ההרצה. תת-טיפוסים כוללים את invalid_argument, out_of_range, domain_error ו-length_error.
  • runtime_error: כישלון שמופיע רק בזמן ריצה ואינו טעות תכנות כשלעצמה. תת-טיפוסים כוללים את range_error, overflow_error ו-underflow_error.

פונקציות ספרייה רבות זורקות אותן בשבילכם. לדוגמה, at() של המכל vector בודקת גבולות וזורקת out_of_range במקום לתת לכם לקרוא מעבר לסוף:

ה-at() הזה הוא המקבילה הבטוחה של v[9]. ה-operator[] הרגיל לא בודק גבולות: קריאה של v[9] כאן היא undefined behavior, לא חריגה. בחירה ב-at() היא הדרך להפוך השחתה שקטה לשגיאה שאפשר לתפוס.

בחרו את הטיפוס שמתאר את השגיאה: invalid_argument כשקורא מעביר משהו חסר היגיון, out_of_range לבעיות של אינדקס או מפתח, runtime_error ל"העולם החיצון נכשל עליי".

כתיבת טיפוס חריגה משלכם

כשאף טיפוס סטנדרטי לא מתאים, כי רוצים לצרף נתונים נוספים או לתפוס עם catch בדיוק את השגיאה שלכם ולא שום דבר אחר, הגדירו מחלקה שיורשת מ-std::exception (או מאחד מתת-הטיפוסים שלה) ודרסו את what(). ירושה מ-std::runtime_error היא הדרך הקלה ביותר, כי היא כבר שומרת את ההודעה ומממשת את what() בשבילכם:

מכיוון ש-NetworkError נושאת קוד סטטוס, המטפל יכול להגיב לו: לנסות שוב על 5xx, לוותר על 4xx. מחרוזת שגיאה בלבד לא הייתה מאפשרת את זה. הטיפוס המותאם גם מאפשר ל-catch (const NetworkError&) לתפוס רק בעיות רשת ולהשאיר את כל השאר למטפל הכללי יותר שמתחתיו.

אם אי פעם יורשים ישירות מ-std::exception (ולא מ-runtime_error), זכרו לדרוס את what() בעצמכם, ולסמן אותה noexcept כדי שתתאים לחתימה של הבסיס:

class ParseError : public std::exception {
public:
    const char* what() const noexcept override {
        return "failed to parse input";
    }
};

זורקים לפי ערך, תופסים לפי הפניה

זה הכלל החשוב ביותר של חריגות ב-C++, וזה שמתחילים טועים בו. זרקו אובייקטים לפי ערך ותפסו אותם לפי הפניית const.

throw runtime_error("oops");            // לפי ערך: נכון
catch (const runtime_error& e) { ... }  // לפי הפניית const: נכון

תפיסה לפי ערך במקום זאת, catch (std::exception e), מעתיקה את החריגה לאובייקט של מחלקת הבסיס ו_חותכת_ את החלק הנגזר (object slicing). אחרי החיתוך, e.what() קוראת למימוש של הבסיס ולא לזה שדרסתם, וההודעה שניסחתם בקפידה נעלמת:

try {
    throw NetworkError(503, "service unavailable");
} catch (std::exception e) {       // לפי ערך: object slicing!
    std::cout << e.what();         // הודעה גנרית, status() נעלמה
}

ההפניה (&) שומרת על הטיפוס הדינמי האמיתי, כך ש-what() הווירטואלית נשלחת נכון ועדיין אפשר לגשת לחברים של המחלקה הנגזרת. הוסיפו const כי אתם רק קוראים את החריגה, לא משנים אותה. לעולם אל תזרקו מצביע (throw new runtime_error(...)): התופס היה צריך לבצע עליו delete, ובאיזה מסלול קוד? זו בדיוק הדליפה שחריגות אמורות למנוע.

הבא בתור: try-catch

עכשיו אתם יודעים ליצור ולזרוק חריגות בנויות היטב ולבחור את הטיפוס הסטנדרטי הנכון לכל כישלון. החצי השני של הסיפור הוא צד התפיסה. העמוד הבא מכסה את try/catch במלואו: סידור כמה בלוקי catch מהספציפי ביותר לכללי ביותר, ה-catch-all catch (...), זריקה מחדש עם throw; חשוף, ואיך RAII (חשבו שוב על smart pointers) מבטיח שהמשאבים שלכם ישוחררו בזמן שה-stack נפרם.

שאלות נפוצות

מה זו חריגה ב-C++?

חריגה (exception) היא אובייקט שמסמן שגיאה שהפונקציה הנוכחית לא יכולה לטפל בה לבד. זורקים אותה עם throw, ה-stack נפרם (ואובייקטים מקומיים מושמדים לאורך הדרך), ובלוק catch מתאים גבוה יותר לוקח פיקוד. זה מפריד בין הקוד שמזהה בעיה לבין הקוד שמחליט מה לעשות איתה.

מה ההבדל בין throw ל-return עבור שגיאות?

ערך return צריך להיבדק על ידי הקורא, וקל לשכוח: התוכנית פשוט ממשיכה עם נתונים שגויים. חריגה שנזרקה לא ניתנת להתעלמות: אם אף אחד לא תופס אותה, התוכנית מסתיימת. חריגות מיועדות לכישלונות אמיתיים (קובץ שלא נפתח, קלט לא תקין); ערכי החזרה עדיין נכונים לתוצאות רגילות, כולל מקרים צפויים של "לא נמצא".

מה עושה המתודה what() בחריגות של C++?

כל מחלקה שנגזרת מ-std::exception מספקת מתודה וירטואלית what() שמחזירה const char* שמתאר את השגיאה. כשתופסים חריגה, קריאה ל-e.what() נותנת את ההודעה הקריאה שאפשר לרשום ללוג או להדפיס. טיפוסי החריגות הסטנדרטיים קובעים אותה מהמחרוזת שמעבירים לבנאי שלהם.

איור של שפות התכנות ב-Coddy

ללמוד תכנות עם Coddy

להתחיל