Menu

Lambda ב-C++: פונקציות אנונימיות עם דוגמאות

כתבו פונקציות קטנות ישירות בתוך הקוד עם lambdas ב-C++: התחביר, איך עובדות לכידות (captures), מתי להשתמש ב-mutable, ומלכודת הלכידה התלויה שנושכת את כולם.

בדף הזה יש עורכים שאפשר להריץ - לערוך, להריץ ולראות את הפלט מיד.

פונקציות שכותבים במקום שבו משתמשים בהן

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

ל-lambda יש צורה ייחודית בת ארבעה חלקים:

[capture](parameters) -> return_type { body }

ה-[] הוא הסימן שאתם מסתכלים על lambda. טיפוס ההחזרה אופציונלי: בדרך כלל הקומפיילר מסיק אותו. הנה הפשוטה ביותר שאפשר:

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

לכידות: גישה לטווח שמסביב

החלק שהופך lambdas ליותר מסתם פונקציות בלי שם הוא רשימת הלכידה, ה-[]. היא מאפשרת ל-lambda להשתמש במשתנים מהטווח שבו היא הוגדרה, ולא רק בפרמטרים שלה.

לכידה לפי ערך עם [x]: ה-lambda מקבלת עותק משלה, קפוא ברגע שבו היא נוצרת.

שימו לב ש-scale(5) הדפיס 50, עם הערך 10 של factor שהיה קיים כשה-lambda נוצרה. לכידה לפי ערך לוקחת תמונת מצב.

לכידה לפי הפניה עם [&x]: ה-lambda מתייחסת למשתנה המקורי, רואה שינויים מאוחרים יותר ויכולה לשנות אותו.

אפשר גם ללכוד את כל מה שה-lambda משתמשת בו עם [=] (הכול לפי ערך) או [&] (הכול לפי הפניה). זה נוח, אבל כתיבה מפורשת, [total] או [&total], מתעדת בדיוק במה ה-lambda נוגעת וקל יותר לחשוב עליה.

מלכודת ההפניה התלויה

לכידה לפי הפניה חזקה ומסוכנת באותה מידה. ההפניה תקפה רק כל עוד המשתנה המקורי חי. אם ה-lambda חיה יותר זמן ממה שהיא לכדה, מקבלים הפניה תלויה והתנהגות לא מוגדרת: התוכנית עלולה לקרוס, להדפיס זבל, או להיראות כאילו היא עובדת במקרה.

זו הטעות הקלאסית: החזרת lambda שלוכדת משתנה מקומי לפי הפניה.

auto makeCounter() {
    int count = 0;
    return [&count]() { return ++count; };  // באג: count מת כאן
}
// ה-lambda שהוחזרה מתייחסת עכשיו לזיכרון שהושמד.

כש-makeCounter חוזרת, המשתנה המקומי count שלה מושמד, אבל ה-lambda עדיין מחזיקה הפניה אליו. קריאה ל-lambda שהוחזרה נוגעת בזיכרון מת. הפתרון הוא ללכוד לפי ערך, כך שה-lambda מחזיקה במצב משלה:

כלל אצבע: לכדו לפי הפניה רק כשה-lambda משמשת מיד ובאופן מקומי (כמו עם האלגוריתמים בהמשך). ברגע ש-lambda נשמרת, מוחזרת או רצה מאוחר יותר, העדיפו לכידה לפי ערך.

mutable וטיפוסי החזרה

שמתם לב ל-mutable בדוגמה האחרונה? כברירת מחדל, לכידה לפי ערך היא const בתוך ה-lambda: אפשר לקרוא את העותק אבל לא לשנות אותו. הוספת mutable מאפשרת ל-lambda לשנות את העותקים שלה בין קריאות.

mutable משפיע רק על העותק הפרטי של ה-lambda: ה-seen החיצוני לא נפגע, וזו כל המטרה של לכידה לפי ערך.

ברוב המקרים הקומפיילר מסיק את טיפוס ההחזרה בלי בעיה. צריך לכתוב אותו במפורש עם -> רק כשיש עמימות, למשל lambda שעשויה להחזיר טיפוסים שונים בענפים שונים:

// בלי -> הקומפיילר לא יכול להחליט בין int ל-double
auto half = [](int n) -> double {
    if (n % 2 == 0) return n / 2;   // int
    return n / 2.0;                 // double
};

lambdas ואלגוריתמים: התמורה האמיתית

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

הדוגמה הנפוצה ביותר היא סדר מיון מותאם:

לכידות זוהרות כאן, כי ה-lambda יכולה למשוך ערך לסנן או לספור לפיו. הקוד הזה סופר כמה מספרים עוברים סף שהמשתמש בחר:

מכיוון שה-lambdas האלה משמשות מיד ולא חיות יותר זמן מהפונקציה שמסביב, גם לכידה לפי הפניה ([&passMark]) הייתה בטוחה כאן, אבל לכידה לפי ערך ברורה באותה מידה ואף פעם לא נשארת תלויה.

הבא בתור: מצביעים

lambdas העלו בשקט שאלה עמוקה יותר: כשלוכדים [&x], ה-lambda מחזיקה ב_מיקום_ של x, והמיקום הזה נשאר תקף רק כל עוד x חי. הרעיון הזה, ערך שמתייחס למקום שבו משהו נמצא בזיכרון, ומה קורה כשהדבר שהוא מצביע עליו נעלם, הוא בדיוק הנושא של העמוד הבא. ניפגש עם מצביעים פנים אל פנים: איך לוקחים כתובת, איך עוקבים אחריה, ואיך אותה בעיה של הפניה תלויה שראיתם עכשיו מופיעה בכל רחבי C++.

שאלות נפוצות

מהי lambda ב-C++?

lambda היא פונקציה אנונימית שאפשר לכתוב ישירות בתוך הקוד, בדיוק במקום שבו משתמשים בה. התחביר הוא [captures](parameters){ body }. היא מושלמת לפעולות קצרות וחד-פעמיות, כמו פונקציית ההשוואה שמעבירים ל-std::sort, בלי להצהיר על פונקציה נפרדת עם שם במקום אחר.

מה ההבדל בין לכידה לפי ערך ללכידה לפי הפניה ב-lambda של C++?

[x] לוכד עותק של x, קפוא ברגע שבו ה-lambda נוצרת. [&x] לוכד הפניה ל-x המקורי, כך שה-lambda רואה שינויים מאוחרים יותר ויכולה לשנות אותו. השתמשו ב-[&] רק כשמובטח שהמשתנים שנלכדו יחיו יותר זמן מה-lambda, אחרת תקבלו הפניה תלויה.

למה ה-lambda שלי ב-C++ טוענת שהיא לא יכולה לשנות משתנה שנלכד?

לכידות לפי ערך הן const בתוך ה-lambda כברירת מחדל. הוסיפו את מילת המפתח mutable, למשל [x]() mutable { x++; }, כדי לאפשר ל-lambda לשנות את העותק שלה. שימו לב שזה משנה רק את העותק של ה-lambda, לא את המשתנה המקורי שבחוץ.

איור של שפות התכנות ב-Coddy

ללמוד תכנות עם Coddy

להתחיל