חידת C++

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

1. הקוד המקורי

הקוד משתמש בספריית Qt אבל אני חושב שאפשר למצוא בעיות דומות גם בלעדיה. נתון הפונקציה הבאה (הניחו שהפונקציות האחרות מוגדרות ותקינות):

void calculateStuff(int i)
{
     QByteArray result = doSomethingComplicated(i);
     const char *resultHex = result.toHex().constData();
     saveResult(i, resultHex);
}

הקוד לא עובד: הוספת הודעות הדפסה בתוך הפונקציה saveResult מוכיחה כי הפונקציה נקראה עם הערך הלא נכון בארגומנט השני. 
הדפסת הערך של המשתנה result לאחר השורה הראשונה בפונקציה מציגה דווקא את התוצאה הצפויה. גם הוספת הודעת ההדפסה הבאה מציגה את התוצאה הנכונה (לא משנה היכן בקוד תמקמו אותה):

qDebug() << result.toHex().constData();

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

2. מה התקלקל

המחלקה QByteArray עוטפת מבנה char * ומייצגת מערך של בתים. היא מציעה מגוון פעולות על מערך זה, למשל הפעולה toHex שמחזירה QByteArray חדש (בו הערכים הם ערכי ה ASCII של ייצוג הקסדצימלי של המערך המקורי), והפעולה constData המחזירה מצביע למידע הפנימי של האובייקט.

מקור הבאג בשמירת המצביע למידע הפנימי של אובייקט זמני.

בהפעלת result.toHex יצרתי אובייקט QByteArray חדש וזמני ואותו לא שמרתי. באותה השורה לקחתי את מבנה הנתונים הפנימי של אובייקט חדש זה למשתנה resultHex. בנקודה זו עדיין הבתים באובייקט החזיקו את הערכים הנכונים.
אבל, כבר במעבר לשורה הבאה הקומפיילר הבין שאין מה לעשות עם אותו אוביקט זמני ומחק אותו. מחיקה של QByteArray מקלקלת את המבנה הפנימי שלו ולכן המשתנה resultHex מקולקל גם.

3. תיקון ברגע

כשמבינים את הבעיה קל גם לתקן. מספיק לתת שם לאובייקט הזמני באמצעות שינוי הקוד באופן הבא:

void calculateStuff(int i)
{
     QByteArray result = doSomethingComplicated(i).toHex();     
     const char *resultHex = result.constData();
     saveResult(i, resultHex);
}

וכעת האובייקט המייצג את הערכים בבסיס הקסדצימלי לא יושמד ואפשר להעביר את resultHex לפונקציה בהצלחה.