1.1 - סביבת עבודה ומודל ביצוע - פתרון
פתרון - אותו ערך אינו בהכרח אותו אובייקט¶
הפונקציה הראשונה משנה אובייקט מקומי שנוצר מהערך של המונה. הפונקציה השנייה ניגשת למונה עצמו דרך reference, ולכן רק היא משנה את הערך שנראה בקורא.
הקוד הבא הוא פתרון מלא לחלק הראשון:
#include <cstdio>
void by_value(int value) {
++value;
}
void by_reference(int& value) {
++value;
}
void observe(const int& value) {
std::printf("observed=%d\n", value);
}
int main() {
static_assert(sizeof(void*) == 8);
int counter = 10;
by_value(counter);
std::printf("after value=%d\n", counter);
by_reference(counter);
std::printf("after reference=%d\n", counter);
observe(counter);
std::printf("int=%zu long=%zu pointer=%zu\n",
sizeof(int), sizeof(long), sizeof(void*));
}
הפלט של שתי השורות הראשונות צריך להיות עשר ואז אחת־עשרה. הפונקציה observe תדפיס אחת־עשרה בלי לשנות את המונה. ניסיון לבצע בתוכה ++value צריך להידחות, מפני שהגישה דרך הפרמטר אינה מאפשרת שינוי.
הסרת const כדי לגרום לקוד להתקמפל אינה פתרון אוטומטי. קודם קובעים אם הפונקציה אמורה לשנות את המידע; החתימה צריכה לייצג את ההחלטה הזאת.
בנייה ובדיקת target¶
בונים ב־x64 Native Tools Command Prompt:
cl /nologo /std:c++20 /EHsc /W4 /Od /Zi main.cpp /Fe:by_value_debug.exe
cl /nologo /std:c++20 /EHsc /W4 /O2 /Zi /FAcs /Fa:by_value.asm main.cpp /Fe:by_value_release.exe
ב־Windows x64 עם MSVC מצפים לגדלים ארבע, ארבע ושמונה. ביעד x86 מצפים שהבדיקה של גודל המצביע תיכשל, לפני שנוצר executable חדש. צריך לוודא שלא מריצים בטעות קובץ ישן שנותר מבנייה קודמת שנכשלה.
המודל LLP64 מתאר את בחירת גדלי הטיפוסים של ה־ABI. הוא אינו אומר שכל יישום 64 ביט רואה את כל הכתובות האפשריות, ואינו מבטיח שמצביע הוא כתובת פיזית.
הוספת קלט¶
מחליפים את אתחול המונה ואת שתי הקריאות שבאות אחריו בקטע הבא בתוך main:
int counter = 0;
if (std::scanf("%d", &counter) != 1) {
std::fputs("invalid input\n", stderr);
return 1;
}
if (counter == 2147483647) {
std::fputs("counter too large\n", stderr);
return 1;
}
by_value(counter);
std::printf("after value=%d\n", counter);
by_reference(counter);
std::printf("after reference=%d\n", counter);
ההגבלה נדרשת כאן מפני שביעד שלנו int בן 32 ביט, והגדלת הערך המרבי עלולה ליצור signed overflow. בתוכנית כללית מעדיפים לקבל את הגבול מתוך std::numeric_limits<int>::max(); המספר המפורש משמש כאן לקישור בין הגודל שנמדד לבין תחום הערכים.
המהדר כבר אינו יכול לחשב מראש את הערך שהמשתמש יבחר. הוא עדיין יכול להסיר פעולה שאין לה השפעה נצפית, למשל ההגדלה של ההעתק בתוך by_value, ולבצע inlining של by_reference. זאת בדיוק הסיבה שחיפוש שם פונקציה באסמבלי אינו מבחן לנכונות התוכנית.
מה נחשב להסבר טוב¶
הסבר טוב מפריד בין שלוש טענות: הפרמטר הראשון הוא העתק לפי כללי השפה, גודל long הוא ארבעה bytes ביעד Windows x64 שבחרנו, והמהדר המסוים עשוי להסיר את הקריאה המפורשת. שתי הטענות הראשונות אינן תלויות בהופעת הוראת call בפלט האסמבלי.
הוספת const מגבילה את הגישה דרך reference מסוים. היא אינה מונעת מ־alias אחר לשנות את אותו אובייקט, אינה מבצעת נעילה ואינה יוצרת סדר בין threadים. את הטענות האלה צריך להוכיח באמצעות חוזי סנכרון כשנגיע לפרק המקביליות.