שאלה ב C++

הפרבולה

New member
שאלה ב C++

הקוד הבא המקומפל עם g++ לא עובר קומפילציה, השגיאה:
error: cannot bind non-const lvalue reference of type ‘A&’ to an rvalue of type ‘A’
קוד:
class A
{
    int data;
public:
    A(int d){data=d;}
};
void func( A  &a)
{    
}
main(){
   func (A(7));
}

הוא דורש שהפרמטר המועבר לפונקציה func יהיה const ( ואז באמת זה עובר ), למה ?
אגב תחת הקומפיילר של VS זה כן עובר קומפילציה.
 

BravoMan

Member
מה שאתה מנסה לעשות, נוגד את הסטנדרט של ++C

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

אתה יכול, לפי התקן, להעביר ייחוס קבוע (const reference) לאובייקט כזה.
כלומר, הפוך את החתימה של func לכזו:
קוד:
void func(const A &a) {
    ...
}

אם אתה תוהה למה זה מתקמפל ב-VS, התשובה היא "הרחבות".
הרבה קומפיילרים, בניהם gcc ו-msvc ממשים הרחבות לשפות C ו-++C שמאפשרות לך לכתוב קוד לא תקין שלא יתקמפל בקומפיילרים אחרים, אבל יעבוד בלי בעיה באותו קומפיילר.

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

נראה כי ל-VS יש הרחבה ספציפית שמאפשרת לך לשבור את הסטנדרט והיא מופעלת ברירת מחדל.

יש דיון רחב בנושא באנגלית כאן:
https://stackoverflow.com/questions/27463785/cant-pass-temporary-object-as-reference
 

הפרבולה

New member
אוקי הבנתי , אגב נראה לי שיכולה לצוץ בעיה בסטנדרט הזה

אם אנחנו כן רוצים שהפונקציה תשנה את האוביקט , כי אנו מתיחסים לזה בהמשך ואז נקרא לפונקציה ככה
קוד:
A a(7);
func(a);
if(a.returnValue== ...){
//....
}
ולפעמים לא איכפת לנו מה השתנה באוביקט ואז נקרא לו כמו מקודם, ככה:
func ( A(7));
אם אני שם const אני מגביל את הפונקציה לא לשנות את האוביקט.

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

BravoMan

Member
הבעיה אינה בסטנדרט, אלא בדיזיין שלך:

למה שתהיה לך פונקציה שתפקידה לשנות את האובייקט שהיא מקבלת, אם מדי פעם אתה מחליט בצורה אקראית לזרוק את השינויים האלה לפח?
&nbsp
הדוגמה הראשונה שהצגת כאן, היכן ש-a אינו זמני, תתקמפל ותעבוד.
&nbsp
הדוגמה השנייה מן הסתם לא, אבל נשאלת השאלה:
למה אתה קורה לפונקציה func במקרה השני?
מה התועלת בשינויים שהיא תעשה באובייקט אם האובייקט לא קיים אחרי שהפונקציה חוזרת וכל העבודה שלה נמחקה?
&nbsp
לפי דוגמת הקוד הראשונה, נראה שעיצוב הנכון של הפונקציה היה צריך להיות עם ערך חוזר שאינו void.
לא ברור לי למה החלטתה לבנות פונקציה שכביכול מחזירה ערך ע"י דחיפה שלו לפרמטר שהיא מקבלת במקום החזרתו כמקובל?
&nbsp
אני יכול רק לנחש, שלפונקציה יש מה שנקרא "side effects" - פעולות שאינן קשורות להחזרת ערך ולא לשינוי של פרמטרים, אבל כן יש להם השפעה על פעולת התוכנית (למשל, הדפסה למסך, כתיבה לקובץ וכו').
&nbsp
במקרה כזה, כנראה שהגיע הזמן לתכנן מחדש:
אולי צריך לדרוס את הפונקציה (override) עם גרסה שפשוט מקבל const int במקום A?
&nbsp
למה לשלוח לפונקציה אובייקט זמני? זה בזבוז מקום ועבודה - האובייקט נוצר, מוחזק בזיכרון, ונזרק לפח.
&nbsp
לחלופין, אם אינך יכול לפצל את הפונקציה לגרסה שמשנה אובייקט וכזאת שלא, או בכלל לתכנן מחדש, תבנה לה מעטפת שתקבל int, תבנה אובייקט מקומי שאינו זמני, ותעביר אותו (by reference) לפונקציה המקורית.
&nbsp
שפת ++C היא שפה "שמנה", עם המון סינטקס ואפשרויות.
איני מכיר לעומק את הסטנדרט שלה ואיני מסכים עם כל מה שהיוצר שלה - ביורן סטראסופ, עשה, אבל במקרה הספציפי הזה אני רואה יותר בעיה עם דוגמאות שימוש שסיפקת מאשר עם הסטנדרט.
 

הפרבולה

New member
יתכן למשל שהפונקציה בעיקר קוראת את התוכן של האוביקט

ועושה עם זה כל מיני דברים ובנוסף כותבת לאיזה משתנה סטטוס שקיים בתוך האוביקט שיש מקומות בקוד שמסתכלים עליו ובמקומות אחרים לא איכפת לנו ממנו.

טוב , אז כמו שאמרתה אפשר לפתור את הבעיה הזאת, למשל על ידי החזרת משהו מהפונקציה דרך ה return או דרך משתנה רפרנס נוסף שאינו const ( בכל מקרה זה עולה קצת בביצועים ובקוד נוסף )

----------
לגבי מה שאמרתה ש:

למה לשלוח לפונקציה אובייקט זמני? זה בזבוז מקום ועבודה - האובייקט נוצר, מוחזק בזיכרון, ונזרק לפח.

אבל גם אם נוסיף את ה const עדין יש לייצר את האוביקט הזמני עם כל העלויות שכרוכות בזה,נכון ?
 

BravoMan

Member
לא הבנתי:

מה בדיוק עולה בביצועים וקוד נוסף?
&nbsp
החזרת ערך אינה משפיע על ביצועי התוכנה כלל.
אם כבר, הצבת ערך חוזר במחסנית יכולה, במקרים מסוימים, להיות זולה יותר מאשר כתיבתו לאיבר של אובייקט שנשמר ב-heap.
&nbsp
אבל זה בכל מקרה "מיקרו אופטימיזציות" שלא רלוונטיות ולא נחוצות בשפות מודרניות עם קומפיילרים מודרניים.
&nbsp
גם לא ברור איזה קוד נוסף נדרש כאן, מעבר להחלפת שורת הצבה בשורה של return?
&nbsp
ייתכן שאני מפספס משהו משום שאני לא מכרי את כל פרטי המימוש, שאתה מן הסתם לא יכול לפרסם.
אבל לפי מה שאני מבין, הכל מתנקז לשאלה של דיזיין.
&nbsp
זה נכון, שאם תיצור אובייקט זמני ותשלח ref קבוע אליו, האובייקט עדיין נוצר.
&nbsp
השאלה האם לשלם את העלות הזו, תלויה בנחיצותו של האובייקט עצמו.
כלומר, האם בכלל יש צורך שהאובייקט יהיה קיים?
כדי לענות על השאלה הזו, צריך לדעת מה הפונקציה שלך עושה עם האובייקט:
&nbsp
כתבת שהפונקציה בעיקר קוראת מידע מהאובייקט.
אם כך, אולי בכלל לא צריך להעביר לה אובייקט, אלא רק את המידע הרלוונטי ממנו?
&nbsp
בהתאם לכמות השדות שהפונקציה צריכה, ייתכן שהגיוני לפצל אותם לפרמטרים ולהעביר רק את הפרמטרים מאשר להתעסק עם יצירת אובייקטים זמניים.
&nbsp
אבל אני רק זורק ניחושים.
בלי לדעת את מפרט הבעיה המלא וה-Use case, דברים שאני מניח שאינך יכול לשתף בפורום, קשה לענות על שאלות דיזיין שכאלה.
&nbsp
אני מניח שיש דרכים לעקוף את מגבלת הסטנדרט.
תלוי בתוצאה שאתה מנסה להשיג, ייתכן שאתה עדיין יכול לקמפל עם msvs בצורה כזו או אחרת, למצוא צירוף פרמטרים ל-gcc שיוותר לך, או לעבור לקומפיילר אחר כמו llvm (אם תמצא אחד שממש הרחבות דומות ל-msvc).
&nbsp
אני בכל זאת ממליץ לכתוב קוד תקני שיתקמפל בכל קומפיילר וכל סביבה, ולחשוב טוב טוב על הדיזיין של הקוד.
אם אתה מוצא את עצמך נלחם בסטנדרט של השפה, או שהשפה דפוקה, או שהשפה לא מתאימה לשימוש שלך, או שהדיזיין שלך דפוק.
&nbsp
אני חושב שאנו יכולים לפסול את שני האופציות הראשונות במקרה הזה:
++C נחשבת לשפה וותיקה וטובה ונפוצה בתעשייה, ואתם משתמשים בה כבר די הרבה זמן.
אז כנראה שהבעיה היא פשוט בעיית דיזיין קטנה, ואולי פשוט קיבלת הזדמנות לשפר את הקוד.
&nbsp
נצל אותה!
&nbsp
בהצלחה!
 

הפרבולה

New member
קודם כל אני כבר פתרתי את הבעיה

הוספתי את ה const כמו שהקומפילר הציע בהודעת השגיאה. רק רצית בעיקר להבין למה תחת VS זה כן מתקמפל ( ועניתה שזה קשור להרחבות ).

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

לגבי החזרת ערך יתכן באמת שזה לא עולה יותר מהצבה למשתנה אוביקט ,.

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

BravoMan

Member
סתם מתוך סקרנות:

אתה יכול לספר באיזו גרסה של VS זה מתקמפל?
שנים לא עבדתי עם VS, אבל לפי הבנתי הצמדות שלהם לסטנדרט משתנה מגרסה לגרסה והיית אמורה להתהדק בגרסאות האחרונות.
&nbsp
בכל אופן, שאלת פרמטרים נפרדים מול רפרנס לאובייקט בודד היא מעניינת:
להעביר 20 פרמטרים לפונקציה זה מעיק.
&nbsp
לא רק מבחינת כמות הדברים שצריך לדחוף למחסנית, אלא גם מבחינת איך הקריאה לפונקציה כזו תראה בקוד, ומה המתכנת יצטרך לכתוב.
&nbsp
מצד שני, אם אתה מייצר אובייקט זמני שאתה מעביר לפונקציה הזו, מהיכן באים ה-20 פרמטרים האלה?
&nbsp
כשאתה מייצר אובייקט זמני כפרמטר לפונקציה, מתבצעות למעשה 2 קריאות במקום אחת:
קודם מתבצעת קריאה לבנאי של האובייקט הזמני, ואז הקריאה לפונקציה המקורית.
&nbsp
אם הבנאי מקבל את כל אותם 20 פרמטרים ומאתחל איתם את האובייקט, לא רק שלא חסכת כלום, אלא למעשה הוספת די הרבה עבודה מיותרת.
&nbsp
אם הבנאי מקבל רק חלק קטן מהפרמטרים ובונה מזה את השאר, נשאלת השאלה:
האם לא הגיוני שהפונקציה תמצא דרך פנימית לעשות את אותה העבודה, בין אם ע"י קריאה לבנאי בעצמה לבין אם ע"י יצוא הקוד המדובר לפונקציית שירות (אולי סטטית או מחוץ למחלקה)?
&nbsp
זה נושא שאפשר לפתח עליו דיון די מעניין בהתאם לנסיבות לאילוצים...
 

הפרבולה

New member
VS 2012

לגבי הסוגיה אם להעביר 20 פרמטרים או לבנות בנאי שגם אותו צריך להתחל, אז במקרה שלי כנראה שעדיף להעביר את הפרמטרים ( מדובר ב 5 ), אבל כרגע זה ממומש עם העברת אוביקט , בכל אופן זה לא קריטי מדובר בהתחול שנעשה פעם ב ... ואני לא רוצה לשנות יותר מידי את הקוד שכבר רץ היטב הרבה זמן ( כל שינוי בקוד מחייב זהירות רבה כדי לא להכניס בגים ).
&nbsp
 
למעלה