Menu

העמסת פונקציות ב-C++: אותו שם, פרמטרים שונים

העמסת פונקציות (function overloading) ב-C++ מאפשרת לכמה פונקציות לחלוק שם אחד, כל עוד רשימות הפרמטרים שלהן שונות. למדו איך overload resolution בוחרת את ההתאמה, למה טיפוס ההחזרה לבד לא נחשב, ואילו מלכודות של עמימות וארגומנטים ברירת מחדל כדאי לעקוף.

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

שם אחד, הרבה גרסאות

לעיתים קרובות צריך את אותה פעולה לסוגים שונים של נתונים: להדפיס int, להדפיס string, להדפיס double. בשפות מסוימות הייתם ממציאים printInt, printString, printDouble. C++ מאפשרת לתת לכולן אותו שם ומבדילה ביניהן לפי הפרמטרים. זו העמסת פונקציות (function overloading).

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

שלוש פונקציות, שם אחד. כל קריאה נוחתת על הגרסה שטיפוס הפרמטר שלה מתאים לארגומנט. זה מה שגורם ל-std::cout << x לעבוד עם ints, doubles ומחרוזות באותה מידה: operator<< מועמס פעמים רבות.

מה נחשב להעמסה שונה

העמסה מובחנת רק לפי רשימת הפרמטרים שלה. אפשר לשנות:

int  area(int side);                 // פרמטר 1
int  area(int width, int height);    // 2 פרמטרים  -> שונה
double area(double r);               // טיפוס שונה -> שונה

void log(string msg, int level);     // הסדר חשוב...
void log(int level, string msg);     // ...ולכן גם זו שונה

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

טיפוס ההחזרה לבד לא מספיק

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

int    convert(double x);   // OK
double convert(double x);   // שגיאה: הגדרה מחדש, רק טיפוס ההחזרה שונה

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

איך overload resolution בוחרת מנצחת

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

  1. התאמה מדויקת (לא צריך המרה).
  2. קידום (promotion, למשל char או short -> int, float -> double).
  3. המרה סטנדרטית (למשל int -> double, double -> int, מצביע למחלקת בסיס).

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

'A' הוא char, אבל קידום ל-int מדורג מעל המרה ל-double, ולכן נקראת ההעמסה של int. כללי הדירוג האלה הם הסיבה ש-overload resolution בדרך כלל "פשוט עושה את הדבר הנכון", ולפעמים מפתיעה אתכם.

מלכודת העמימות

אם שתי העמסות טובות באותה מידה, אף אחת לא טובה יותר בבירור, המהדר מסרב לנחש ומדווח על קריאה עמומה (ambiguous call). המקרה הקלאסי הוא שתי העמסות שכל אחת מהן צריכה המרה באותה דרגה:

void f(int x);
void f(double x);

f(0L);   // שגיאה: עמום, long -> int ו-long -> double הן המרות באותה דרגה

לא int ולא double הם התאמה מדויקת ל-long, ושתי ההמרות נמצאות באותה דרגה, ולכן הקריאה עמומה. יש שני תיקונים נקיים:

הפתעה קשורה: העברת ליטרל מחרוזת. void g(const string&) ו-void g(bool) יתחרו שתיהן על g("hi"), ו-bool עלולה לנצח, כי const char* מומר ל-bool (לא null -> true) בפחות צעדים מאשר בניית std::string. אם אי פעם תראו ליטרל מחרוזת שקורא באופן מסתורי להעמסה של bool, זו הסיבה: הוסיפו העמסה של const char* או const string& שתיקח את ההתאמה המדויקת.

העמסה וארגומנטים ברירת מחדל לא מסתדרים טוב

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

void connect(string host, int port = 8080);   // אפשר לקרוא לה עם ארגומנט אחד
void connect(string host);                     // גם לה אפשר לקרוא עם ארגומנט אחד

connect("localhost");   // שגיאה: עמום, שתיהן מתאימות לארגומנט יחיד

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

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

הבא בתור: Lambdas

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

שאלות נפוצות

מה זו העמסת פונקציות ב-C++?

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

האם שתי פונקציות ב-C++ יכולות להיבדל רק בטיפוס ההחזרה?

לא. העמסות חייבות להיבדל ברשימת הפרמטרים. int f(int) ו-double f(int) הן שגיאת הידור: טיפוס ההחזרה אינו חלק מהחתימה שמשמשת ל-overload resolution, כי המהדר בוחר את ההעמסה לפי הארגומנטים בנקודת הקריאה, עוד לפני שמשתמשים בערך המוחזר.

מה גורם לשגיאת "ambiguous call" עם פונקציות מועמסות?

זה קורה כששתי העמסות הן התאמות טובות באותה מידה והמהדר לא יכול להעדיף אחת. מקרה קלאסי הוא f(int) ו-f(double) שנקראות עם f(0L) (שהוא long), כששתיהן דורשות המרה באותה דרגה. תקנו את זה על ידי הוספת העמסה עם התאמה מדויקת או המרת הארגומנט לטיפוס שאתם רוצים.

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

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

להתחיל