Menu

Templates ב-C++: פונקציות ומחלקות גנריות

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

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

כותבים פעם אחת, משתמשים לכל טיפוס

בדף הקודם מיינתם vector<int> עם std::sort. אבל std::sort ממיין גם vector<string>, vector<double> או מערך של structs משלכם, בלי שמישהו כתב sort נפרד לכל אחד. זה לא קסם וזה לא overloading. זה template: קטע קוד אחד שהמהדר משתמש בו מחדש לכל טיפוס שתעבירו לו.

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

int    maximum(int a, int b)       { return a > b ? a : b; }
double maximum(double a, double b) { return a > b ? a : b; }
string maximum(string a, string b) { return a > b ? a : b; }

הגופים זהים. רק הטיפוסים שונים. template מאפשר לומר "זה עובד לכל טיפוס T" ולכתוב את זה פעם אחת.

Function templates

הופכים פונקציה ל-template על ידי הוספת template <typename T> לפניה ושימוש ב-T בכל מקום שבו בדרך כלל היה טיפוס קונקרטי.

שימו לב שאף פעם לא כתבתם maximum<int> או maximum<double>. המהדר מסתכל על הארגומנטים ומבין מה T צריך להיות: זו template argument deduction (הסקת ארגומנטים של template). כל טיפוס שונה שקוראים איתו גורם למהדר לייצר (instantiate) מאחורי הקלעים פונקציה קונקרטית נפרדת.

אפשר לכתוב את הטיפוס במפורש, בסוגריים משולשים, כשההסקה לא יכולה לעזור:

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

maximum(3, 7.5);   // שגיאה: T הוא int או double? המהדר מסרב לנחש.

אפשר לתקן את זה בכתיבה מפורשת, maximum<double>(3, 7.5), או במתן פרמטר טיפוס נפרד לכל פרמטר, וזה מה שנעשה עכשיו.

כמה פרמטרי טיפוס

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

כשטיפוס ההחזרה תלוי בפרמטרים, תנו למהדר להבין אותו עם auto (מ-C++14 ומעלה), שמשתלב באופן טבעי עם templates:

Class templates

templates הם לא רק לפונקציות: אפשר להפוך מחלקות שלמות ל-templates. כך בדיוק עובדים הקונטיינרים הסטנדרטיים: vector<int>, ה-map של מפתח וערך ו-pair<A, B> הם כולם class templates. כותבים את מבנה הנתונים פעם אחת, והוא מאחסן כל טיפוס שתגדירו לו כפרמטר.

הנה Box גנרי קטנטן שמחזיק ערך אחד מכל טיפוס:

ההבדל המרכזי מ-function templates: עם class template בדרך כלל צריך לספק את הטיפוס בסוגריים משולשים, Box<int>, כי בתקנים ישנים אין ארגומנטים של בנאי להסיק מהם. (C++17 הוסיפה class template argument deduction, כך ש-Box b(42); עובד גם כן, אבל כתיבה מפורשת תמיד בטוחה וקריאה.)

השגיאות יהיו ענקיות, והנה הסיבה

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

template <typename T>
T maximum(T a, T b) {
    return a > b ? a : b;   // דורש ש-T יתמוך באופרטור >
}

struct Point { int x, y; };

// maximum(Point{1,2}, Point{3,4});
// שגיאה: אין operator > ל-Point. ההודעה מזכירה את Point וגם
// מצטטת את כל הפונקציה הזו, ולעיתים קרובות משתרעת על שורות רבות.

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

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

התיקון האמיתי הוא לוודא שהטיפוס שלכם תומך בכל מה שה-template צריך: עבור maximum, זה אומר להעמיס את operator> עבור Point, נושא לדף מאוחר יותר. concepts של C++20 המודרנית יכולים להקדים את השגיאות האלה ולהפוך אותן לקריאות, אבל מודל ההצבה שמתחת נשאר אותו דבר.

הבא בתור: מחלקות

זה עתה בניתם class template בשם Box, מחלקה עם נתונים פרטיים, בנאי ופונקציות חברות, בזמן שהתמקדתם ב-templates. הדף הבא מאט ומלמד מחלקות כמו שצריך: איך מאגדים נתונים עם הפונקציות שפועלות עליהם, מה public ו-private באמת שולטים בו, ואיך פונקציות חברות ניגשות למצב של האובייקט עצמו. templates ומחלקות משתלבים כל הזמן ב-C++ אמיתית, אז שליטה טובה במחלקות הופכת קוד גנרי להרבה יותר קל לכתיבה.

שאלות נפוצות

מה זה template ב-C++?

template (תבנית) הוא שרטוט שמאפשר לכתוב פונקציה או מחלקה פעם אחת, והמהדר מייצר גרסה לכל טיפוס שמשתמשים בו. כותבים template <typename T> ואז משתמשים ב-T כממלא מקום של הטיפוס האמיתי. המהדר מייצר גרסה קונקרטית: זה נקרא instantiation.

מה ההבדל בין typename ל-class ב-template של C++?

ברשימת הפרמטרים של template, template <typename T> ו-template <class T> אומרים בדיוק אותו דבר. היום בדרך כלל מעדיפים את typename כי הוא מדויק יותר: T יכול להיות כל טיפוס, לא רק מחלקה. בחירת מילת המפתח לא משפיעה בכלל על הקוד שנוצר.

למה הודעות השגיאה של templates ב-C++ כל כך ארוכות?

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

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

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

להתחיל