העברה מושלמת
חלק מהיחידה תכנות מונחה עצמים במסלול ה-C++ של Coddy. שיעור 84 מתוך 104.
בעת כתיבת פונקציות תבנית שמקבלות ארגומנטים ומעבירות אותם לפונקציות אחרות, מתעוררת בעיה: איך משמרים את השאלה אם הארגומנט המקורי היה lvalue או rvalue? העברה מושלמת פותרת זאת באמצעות העברת הארגומנטים בדיוק כפי שהתקבלו, תוך שמירה על קטגוריית הערך שלהם.
המרכיבים העיקריים הם הפניות מעבירות (נכתבות בתור T&& בהקשר של תבנית) ו־std::forward. הפניה מעבירה יכולה להיקשר הן ל־lvalues והן ל־rvalues, ו־std::forward ממירה את הארגומנט בחזרה לסוג המקורי שלו באופן מותנה:
#include <iostream>
#include <utility>
void process(int& x) { std::cout << "lvalue: " << x << "\n"; }
void process(int&& x) { std::cout << "rvalue: " << x << "\n"; }
template<typename T>
void wrapper(T&& arg) {
process(std::forward<T>(arg));
}
int main() {
int n = 10;
wrapper(n); // קורא ל־process(int&) - ה־lvalue נשמר
wrapper(20); // קורא ל־process(int&&) - ה־rvalue נשמר
}ללא std::forward, פרמטרים בעלי שם הם תמיד ערכי lvalue בתוך הפונקציה, גם אם הועבר ערך rvalue. הדבר ימנע מסמנטיקת העברה לעבוד כראוי בעת העברה לבנאים או לפונקציות אחרות.
העברה מושלמת חיונית לכתיבת פונקציות מפעל, מחלקות מעטפת וכל קוד גנרי שצריך ליצור אובייקטים או להפעיל פונקציות תוך שימור כוונת הקורא לגבי העברות לעומת העתקות.
אתגר
קלבואו נבנה פונקציית מפעל שמדגימה העברה מושלמת באמצעות יצירת אובייקטים תוך שמירה על קטגוריית הערך של הארגומנטים שהועברו אליה. תיצרו פונקציית make_object גנרית שמעבירה ארגומנטים לבנאי של מחלקה, ומבטיחה שערכי l יועתקו וערכי r יועברו בהעברה.
תארגנו את הקוד שלכם בשלושה קבצים:
Widget.h: הגדירו מחלקתWidgetשעוקבת אחר האופן שבו היא מקבלת את הנתונים שלה.המחלקה
Widgetשלכם צריכה לאחסן שם מסוגstd::stringוערך מסוגint. ספקו שני בנאים:- בנאי שמקבל
const std::string&(הפניה לערך l) ו-int: מדפיסWidget constructed (copy): [name] - בנאי שמקבל
std::string&&(הפניה לערך r) ו-int: מדפיסWidget constructed (move): [name]ומעביר את המחרוזת בהעברה
הוסיפו גם מתודת
display()שמדפיסה[name]: [value].- בנאי שמקבל
Factory.h: צרו את פונקציית המפעל שלכם להעברה מושלמת.כתבו פונקציית תבנית
make_objectשמקבלת הפניה להעברה עבור השם ו-intרגיל עבור הערך. השתמשו ב-std::forwardכדי להעביר את ארגומנט השם לבנאי שלWidgetתוך שמירה על קטגוריית הערך המקורית שלו.הפונקציה צריכה להחזיר את אובייקט ה-
Widgetשנבנה, לפי ערך.main.cpp: קראו שתי קלטים:- שם (מחרוזת)
- ערך (מספר שלם)
הדגימו העברה מושלמת באמצעות יצירת שני אובייקטים מסוג Widget:
- ראשית, אחסנו את השם במשתנה מקומי והעבירו אותו ל-
make_objectכערך l - שנית, העבירו מחרוזת זמנית (אותו שם בצירוף
"_temp") ישירות ל-make_objectכערך r
לאחר יצירת כל אובייקט, קראו למתודת
display()שלו.
לדוגמה, עם הקלטים Gadget ו-42:
Widget constructed (copy): Gadget
Gadget: 42
Widget constructed (move): Gadget_temp
Gadget_temp: 42עם הקלטים Device ו-100:
Widget constructed (copy): Device
Device: 100
Widget constructed (move): Device_temp
Device_temp: 100התובנה המרכזית היא שפונקציית make_object שלכם לא יודעת אם היא קיבלה ערך l או ערך r, אבל באמצעות שימוש ב-std::forward, היא שומרת על המידע הזה בעת הקריאה לבנאי של Widget. כך פתרון העמסת הבנאים יכול לבחור בגרסה המתאימה, ולאפשר העברות יעילות עבור ערכים זמניים תוך העתקה בטוחה של משתנים בעלי שם.
נסו בעצמכם
#include <iostream>
#include <string>
#include "Widget.h"
#include "Factory.h"
using namespace std;
int main() {
// קראו את הקלט
string name;
int value;
cin >> name >> value;
// TODO: צרו את הווידג'ט הראשון על ידי העברת name כ-lvalue
// שמרו את name במשתנה מקומי והעבירו אותו ל-make_object
// TODO: קראו ל-display() על הווידג'ט הראשון
// TODO: צרו את הווידג'ט השני על ידי העברת מחרוזת זמנית כ-rvalue
// העבירו את (name + "_temp") ישירות ל-make_object
// TODO: קראו ל-display() על הווידג'ט השני
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++ אונליין