למה יש לי תחושה שזו כבר פעם רביעית?
בהעברת תוכנית מקוד מקור לקובץ ריצה יש כמה שלבים. העיקריים שבהם לעניין הזה הם הפרהפרוססור, קומפילציה והלינק - קדם-עיבוד, הידור וקישור בעברית. שלב ההידור מתבצע על כל "יחידת קומפילציה" - Compilation Unit -
בנפרד. "יחידת קומפילציה" היא, למעשה, קובץ מקור לאחר ה-pre-processor, כלומר לאחר שכל הקבצים שהכללת ב-#include הוכנסו לתוך הקובץ, וכל המקרואים שהגדרת ב-#define הומרו לערכים שלהם, ולאחר שכל הקטעים שהיו בתוך קומפילציה מותנית (#ifdef וכד') הוצאו מהקובץ. הקובץ הזה (שניתן לייצר אותו עם gcc -E, או עם ה-flag של MSDEV - /P /E) הוא מה שעובר לקומפיילר. בשלב הזה אין לקומפיילר (ואני מתכוון לחלק שמבצע הידור בלבד) שום מושג מה יש בקבצי מקור אחרים. עכשיו, הקומפיילר מקמפל את הקובץ. משתנים ופונקציות שהוא מוצא בקובץ עצמו, הוא מכוון פנימית. מה שהוא לא מוצא, הוא משאיר מקום פנוי בטבלה, ומחכה ללינקר שישלים. דוגמא:
// pr_sq.h void print_square(int i); // pr_sq.cpp #include <iostream> #include "pr_sq.h" void print_square(int i) { std::cout << i * i << std::endl; } // main.cpp #include "pr_sq.h" int i; // It's global in purpose int main() { i = 3; print_square(i); return 0; }
מה קורה פה? ה-Pre-Processor עובר על sq_pr.cpp, מכניס (ממש מכניס!) את ה-h ואת iostream לתוך הקובץ. הקומפיילר מקמפל ויוצר קובץ אוביקט. ה-Pre-Processor עובר על main.cpp ויוצר קובץ אוביקט נוסף. אם תשים לב, i אינו מוגדר ב-scope של main ולכן, הקומפיילר יחפש אותו קודם כל בקובץ, ימצא אותו ב-scope הגלובלי וימקם אותו בטבלת ה-symbols שלו. מצד שני הוא יחפש את void print_square(int). הוא ימצא את ההצהרה בגלל שהכללת את pr_sq.h אבל לא את הסימבול עצמו. לכן, הוא לא יעצור בשלב הקומפילציה עם טעות, ויחכה ללינקר שישלים את החסר. הלינקר יקשר את שני קבצי האוביקטים הנ"ל והתוכנית תרוץ והכל יופי. ועכשיו ל-templates. כשמגדירים פונקצית template (או מחלקה, זה לא חשוב במקרה הזה), הקומפיילר מייצר instances שלה לפי הצורך. כלומר, אם אתה מגדיר:
// pr_sq.h template <tpyename T> void print_square(T t); // pr_sq.cpp #include <iostream> #include "pr_sq.h" template <typename T> void print_square(T t) { std::cout << t * t << std::endl; } // main.cpp #include "pr_sq.h" int main() { int i = 3; print_square(i); double d = 3.3; print_square(d); return 0; }
אתה מצפה שהקומפיילר ייצור לך פונקציות מתאימות. אבל, לקומפיילר אין מושג מה ליצור! למה? בקובץ שמגדיר את הפונקציה, לקומפיילר אין שום מוטיבציה ליצור instances כי אין שום שימוש בפונקציה הזו (ואתה לא יכול לצפות שהוא ינחש לאלו סוגי טיפוסים הוא אמור ליצור). מצד שני, בקובץ שמשתמש בפונקציה, הקומפיילר מחפש הגדרה של print_square שמקבלת int ומחזירה void ולא מוצא. לכן, בשני המקומות הוא משאיר את זה ללינקר, שכמובן גם לא מוצא בשום קובץ symbol מתאים לפונצקיה הזו, ולכן אתה נשאר עם undefined symbol והתוכנית נופלת. מקווה שהבעיה ברורה. פתרונות: 1. להכניס גם את הגדרת ה-template לקובץ h. כיוון שקובץ כזה מוכלל ממש ב-cpp, הקומפיילר כבר יראה הכל בזמן קומפילציה וייצור פונקתיות מתאימות ל-int ול-double. 2. ליצור שימוש מלאכותי בקובץ cpp של הגדרת הפונקציה, כלומר אחרי הגדרת הופנקציה, להוסיף:
template void print_square<int>(int t); template void print_square<double>(double t);
זה מה שנקרא explicit instansiation. היתרון, זה ממשיך לשמור על הפרדה בין הכרזה למימוש-הגדרה. החסרון, אתה צריך לעשות זאת לכל סוג משתנה שתשתמש בו אי-פעם, דבר שדי מנוגד לכל הרעיון של templates. 3. לחכות קצת עד שיותר קומפיילרים-לינקרים יתמכו במילה השמורה export, שתעשה את הקסם הזה לבד. הפתרון הפופולרי ביותר הוא 1. אם יש שאלות תשאל.