סמנטיקת העברה ו־rvalues
חלק מהיחידה תכנות מונחה עצמים במסלול ה-C++ של Coddy. שיעור 83 מתוך 104.
ב-C++, כל ביטוי הוא lvalue (בעל זהות מתמשכת וניתן לקבל את כתובתו) או rvalue (זמני ועומד להיהרס). הבנת ההבחנה הזאת מאפשרת להשתמש בסמנטיקת העברה — אופטימיזציה רבת־עוצמה שמונעת העתקה מיותרת.
הפניה לערך rvalue, המוצהרת באמצעות &&, נקשרת במיוחד לאובייקטים זמניים. כך אפשר "לגנוב" משאבים מאובייקטים שעומדים להיעלם בכל מקרה:
#include <iostream>
#include <utility>
class Buffer {
int* data;
size_t size;
public:
Buffer(size_t s) : data(new int[s]), size(s) {
std::cout << "Constructed\n";
}
// בנאי העברה - גוזל משאבים
Buffer(Buffer&& other) noexcept
: data(other.data), size(other.size) {
other.data = nullptr; // משאיר את המקור במצב תקין
other.size = 0;
std::cout << "Moved\n";
}
~Buffer() { delete[] data; }
};
int main() {
Buffer b1(1000);
Buffer b2(std::move(b1)); // מפעיל את בנאי ההעברה
}הפונקציה std::move לא באמת מעבירה שום דבר — היא פשוט מבצעת המרה של lvalue להפניה ל-rvalue, ובכך מסמנת שאתה מוכן לוותר על המשאבים של האובייקט. ההעברה בפועל מתרחשת בבנאי ההעברה או באופרטור ההשמה בהעברה.
סמנטיקת העברה משפרת באופן דרמטי את הביצועים בעבודה עם אובייקטים עתירי משאבים כמו מכולות או מחרוזות. במקום להעתיק לעומק מגה־בייטים של נתונים, פשוט מעבירים את הבעלות על המצביע — פעולה שזמן הביצוע שלה קבוע ללא קשר לגודל.
אתגר
קלבואו נבנה מחלקת DataBuffer שמנהלת משאבים ומדגימה סמנטיקת העברה בפעולה. תוכלו לראות כיצד העברת משאבים במקום העתקתם יכולה לשפר משמעותית את היעילות בעת העברת בעלות על זיכרון שהוקצה באופן דינמי.
תארגנו את הקוד שלכם בשלושה קבצים:
DataBuffer.h: הגדירו מחלקתDataBufferשמנהלת מערך של מספרים שלמים שהוקצה באופן דינמי.במחלקה שלכם צריכים להיות חברים פרטיים עבור מצביע הנתונים (
int*), הגודל (size_t) והשם (std::string), כדי לעזור לעקוב אחר זהותם של המאגרים במהלך הפעולות.הצהירו על הדברים הבאים:
- בנאי שמקבל שם מסוג
std::stringוגודל מסוגsize_t, מקצה את המערך ומדפיס:[name] constructed with size [size] - בנאי העברה שמקבל הפניה לערך ימני, משתלט על המשאבים ומדפיס:
[name] moved from [source_name](כאשר המאגר שאליו מתבצעת ההעברה מקבל את שם המקור) - הורס שמדפיס
[name] destroyed(אוempty destroyedאם בוצעה העברה מהמאגר) - שיטת
getSize()שמחזירה את הגודל הנוכחי - שיטת
getName()שמחזירה את שם המאגר
זכרו לסמן את בנאי ההעברה בתור
noexceptולהשאיר את אובייקט המקור במצב ריק תקין (nullptr, size 0, name "empty").- בנאי שמקבל שם מסוג
DataBuffer.cpp: ממשו את כל השיטות שהוצהרו בקובץ הכותרת. כאשר ההורס מופעל, מחקו את הנתונים רק אם המצביע אינו null. כללו את<iostream>עבור פלט.main.cpp: קראו שני נתונים:- שם עבור המאגר שלכם (מחרוזת)
- גודל עבור המאגר שלכם (מספר שלם)
צרו
DataBufferעם השם והגודל הנתונים. לאחר מכן צרו מאגר שני באמצעות העברה מהראשון עםstd::move(). אחרי ההעברה, הדפיסו את מצבם של שני המאגרים:Original: [name] size=[size]New: [name] size=[size]
כללו את
<utility>עבורstd::move.
לדוגמה, עבור הקלטים Alpha ו-100:
Alpha constructed with size 100
Alpha moved from Alpha
Original: empty size=0
New: Alpha size=100
Alpha destroyed
empty destroyedעבור הקלטים Buffer ו-50:
Buffer constructed with size 50
Buffer moved from Buffer
Original: empty size=0
New: Buffer size=50
Buffer destroyed
empty destroyedשימו לב כיצד בנאי ההעברה מעביר בעלות על הזיכרון שהוקצה בלי להעתיק נתונים. המאגר המקורי נשאר במצב ריק אך תקין, וכאשר שני המאגרים נהרסים בסוף התוכנית, רק המאגר שעדיין מחזיק בבעלות על הזיכרון מוחק אותו בפועל.
נסו בעצמכם
#include <iostream>
#include <string>
#include <utility>
#include "DataBuffer.h"
int main() {
std::string name;
int size;
std::cin >> name;
std::cin >> size;
// TODO: צרו DataBuffer עם השם והגודל הנתונים
// TODO: צרו מאגר שני על ידי העברה מהראשון באמצעות std::move()
// TODO: הדפיסו את המצב של שני המאגרים:
// מקורי: [name] size=[size]
// חדש: [name] size=[size]
return 0;
}
השיעור הזה כולל חידון קצר. התחילו את השיעור כדי לענות עליו ולעקוב אחרי ההתקדמות.
כל השיעורים ביחידה תכנות מונחה עצמים
1יסודות OOP
קבצים חיצונייםבנייה וקומפילציה של C++קובצי Header וקובצי מקורמרחבי שמות והיקףמבוא ל-OOP ב-C++מחלקות לעומת אובייקטיםמצביע thisמתודות (פונקציות איבר)תכונות (חברי נתונים)יסודות הבנאים וההורסיםסיכום – מחשבון פשוט4מאפייני מחלקה
חברי מופע לעומת חברים סטטייםגטרים וסטריםפונקציות איבר קבועותמילת המפתח mutableמתודות ומשתנים סטטייםפונקציות ומחלקות ידידותסיכום – מנהל חשבונות בנק7ירושה
ירושה בסיסיתרמות גישה בירושהסדר הקריאות לבנאי ולמפרקדריסת מתודותפונקציות וירטואליות ו־VTableירושה מרובהירושה וירטואליתסיכום – היררכיית עובדים2ניהול זיכרון
זיכרון Stack לעומת Heapמצביעים והפניותזיכרון דינמי (new/delete)מצביעים חכמים ב-C++RAII ב-C++סיכום – מנהל מערכים דינמיים5כימוס
מצייני גישה ב-C++מצייני גישה לעומקהסתרת מידעמבנה לעומת מחלקהמחלקות מקוננות ופנימיותסיכום – מערכת רשומות תלמידים8רב־צורתיות
רב־צורתיות בזמן קומפילציה לעומת בזמן ריצההעמסת פונקציותפונקציות וירטואליות — מבט נוסףפונקציות וירטואליות טהורותמחלקות מופשטותתכנון ממשקים ב־C++המרה דינמית ו־RTTIסיכום — מחשבון צורות3בנאים והורסים
בנאי ברירת מחדלבנאי עם פרמטריםבנאי העתקהבנאי העברהרשימות אתחול בבנאיםבנאים מאציליםמעמיקים בנושא הורסיםכלל השלושה / החמישה / האפססיכום – מחלקת String6העמסת אופרטורים
מבוא להעמסת אופרטוריםהעמסת אופרטורים אריתמטייםהעמסת אופרטורי השוואהאופרטורי זרםהעמסת אופרטור השמההעמסת האופרטורים [] ו־()אופרטורים להמרת טיפוסיםסיכום – מחלקת Matrix9תבניות
תבניות פונקציהתבניות מחלקההתמחות תבניותתבניות ארגומנטים משתנותיסודות SFINAE ותכונות טיפוסיםסיכום – מכל גנרי12תכונות מודרניות ב־C++
סמנטיקת העברה ו־rvaluesהעברה מושלמתביטויי Lambda לעומקstd::function ו־std::bindconstexpr ו־constevalקישור מובנהoptional, variant, anyתרגלו בעצמכם: קומפיילר C++ אונליין