כשאנו כותבים מחלקה, ראינו כי אנו חייבים לקבוע לה טיפוסים לשדות.
ראינו גם כי בנינו של . אבל מה אם נרצה עבור או או כל דבר אחר? אנו לא רוצים להעתיק את הקוד מלאא פעמים.
לשם כך, נותנת לנו את האפשרות להגדיר שבלונה של מחלקה אשר בה נוכל להשתמש בטיפוס אשר לו נקרא בשם מסויים ואז כאשר אנו יוצרים את הקוד אנו נוכל להגיד תקרא ל- של או או כל דבר אחר.
template<class T>
T max(const T& a, const T& b)
{
return a > b ? a : b;
}
int main()
{
int a = 7, b = 5;
double d1 = 2.0, d2 = 3.0;
cout << max(a, b) << endl;
cout << max(d1, d2) << endl;
return 0;
}
ניתן לחשוב על מה שקורה פה כהמהדר יוצר מלא ההעמסות עבור הפונקציה .
איך אנו יודעים כי עבור יש את הפעולה ? ובכן, זוהי הבעיה הגדולה עם כי ל- לא איכפת ⇐ לא יתקמפל.
("ב- הוסיפו קונספט בשם קונספט" - גל ללוש (הקונספט מאפשר להגביל את אבל לא נלמד עליו בקרוס הזה או, ככל הנראה, באף קורס בטכניון כרגע))
template<class T>
T maxElement(const T* arr, int size)
{
T result = arr[0];
for (int i = 0; i < size; ++i)
reult = max(result, arr[i]); // from the earlier example
return result
}
ניתן להגיד במפורש למהדר איזה סוג משתנה צריך להשתמש בו כך:
int n1 = 7, n2 = 5;
double d1 = 2.0, d2 = 3.0;
cout << max<int>(n1, n2);
cout << max<double>(d1, d2);
cout << max(n1, n2);
cout << max(d1, d2);
המהדר לא יבחר בעצמו את הפונקציה המתאימה רק על בסיס ערך ההחזרה.
template<class T, class S>
T convert(const S& s)
{
return T(s);
}
int main()
{
int n = 7;
double d1 = convert(n); // ERROR
double d2 = convert<double>(n);
double d3 = convert<double, int>(n);
}
גם גל מסכים שהדוגמה הזו גרועה שכן אף אחד בחיים לא יכתוב קוד כזה.
מחלקות גנריות
template<class T>
class Array
{
T* data;
int size;
public:
explicit Array(int size);
Array(const Array& a);
~Array();
Array& operator=(const Array& a);
int size() const;
T& operator[](int index);
const T& operator[](int index) const;
};
template<class T>
T& Array<T>::operator[](int index)
{
if (index < 0 || index >= size)
throw out_of_range("Bad index");
return data[index];
}
כאשר אנו כתובים מחלקת - כל הקוד, גם המימוש, צריך להיות בקובץ - זה חשוב מאוד!
הוא לא אותה מחלקה כמו .
הן מתנהגות כמחלקות שונות לגמרי שאינן קשורות.
נשים לב כי אם ננסה לממש תהיה בעיה אחת: ב- אם אנו הורסים את האובייקט שלנו ואז איפשהו בבנאי ההעתקה הוא נכשל, אנחנו בבעיה רצינית. לכן, נצור פונקציה ונשתמש בה כך:
template<class T>
void Array<T>::swap(Array<T>& a1, Array<T>& a2)
{
std::swap(a1.size, a2.size);
std::swap(a1.data, a2.data);
}
template<class T>
Array<T>& Array<T>::operator=(const Array<T>& ar)
{
Array<T> temp(ar);
swap(*this, temp);
return *this; // automaticly calls the destructor of temp
}
אופרטור חץ
כשראינו איטרטורים הבנו שהם מייצגים סוג של פויינטר אך לא מימשנו את אחת ההתנהגויות של פויינטר רגיל והיא אופרטור חץ: (בהמשך אנו נראה ושם נראה למה זה שימושי בכלל)
כעת, פשוט נממש אחד שכזה.
template<class T>
const T* Set<T>::Iterator::operator->() const
{
if (index < 0 || index >= set->getSize())
throw out_of_range("Bad index");
return &(set->data[index]);
}
הגבלות על
אם אנחנו לא מניחים שום דבר על אנחנו די לא יכולים לעשות איתו כלום:
- אנחנו לא יכולים לבנות אותו או להעתיק אותו כי אנחנו לא יודעים איזה בנאים יש לו.
- בכך, אנחנו לא יכולים לקבל אותו כפרמטר לפונקציה כי אנחנו לא יודעים אם יש לו בנאי העתקה או הורס.
- אנחנו לא יכולים להשתמש בפונקציות עליו כי אנחנו לא יודעים אילו פונקציות ממומשות עבורו.
ב- אנו לא יכולים להכתיב הגדרות כאלו למהדר ואנו פשוט צריכים לבצע הנחות מסויימות כדי להשתמש ב- כמו שצריך.
כאשר אנו לוקחים אובייקט אשר אין לו את כל הפונקציות הדרושות, המהדר פשוט לא ייצר חלק מהפונקציות - אלו שמשתמשות בפונקציות שאין.
הפונקציות שלא משתמשות בפונקציות שאין יווצרו ונוכל להשתמש בהן.
מחלקות שהן פונקציות
מחלקה שמיישמת את האופרטור נקרא אובייקט פונקציה.
מדי פעם נרצה לקרוא לפונקציה עם איזשהו קונפיגורציה לא מוכרת. לשם כך נוכל ליצור מחלקה אשר תממש את האופרטור סוגריים ובכך תהיה "פונקציה" (לא באמת פונקציה)
class DividesBy
{
int n;
public:
DividesBy(int n) : n(n) {}
bool operator()(int number) const
{
return number % n == 0;
}
};
int main()
{
DividesBy isEven(2); // constructor call
// checking if things are divided by 2
cout << isEven(9) << endl; // operator()
cout << isEven(8) << endl;
// checking if things are divided by 3
DividesBy isTriple(3);
cout << isTriple(9) << endl;
cout << isTriple(8) << endl;
return 0;
}
איפה שנראה שזה יהיה שימושי ביותר הוא אם נרצה בתוך מבנה נתונים לבצע מחיקה של חלק מהאיברים באמצעות תנאי מסויים אשר בודק בהתאם לדברים - נראה דוגמה קונקרטית ב-.
לפני כן נראה עוד מחלקת עזר נחמדה:
class Between
{
int min, max;
public:
Between(int min, int max) : min(min), max(max) { }
bool operator()(int val) const
{
return min <= val && val <= max;
}
};
כעת נוכל לבנות לתוך קבוצה שכזה שאנו יכולים להפעיל עם כל פונקציה על מנת למיין איברים מהקבוצה.
template <class T>
class Set
{
public:
// ...
template<class Condition>
Set filter(const Condition& c) const;
};
template<class T>
template<class Condition>
Set<T> Set<T>::filter(Condition c) const
{
Set result;
for (const T& elem : *this)
{
if (c(elem))
{
result.add(elem);
}
}
return result;
}
כעת נוכל לראות את השימוש:
void test(const Set<int>& s)
{
DividesBy isEven(2);
cout << s.filter(isEven) << endl;
int x1, x2;
cin >> x1 >> x2;
cout << s.filter(Between(x1, x2)) << endl;
}
פונקציות למדה
זה מבאס לכתוב מחלקה שלמה כל פעם שאחנו רוצים פונקציה אחת ולכן ב- הוסיפו את הקונספט בשם אשר מקצרים לנו את כל הבלגן.
כעת, במקום להגדיר מחלקה בשם או נוכל לכתוב כך:
void testWithLambdas(const Set<int>& s)
{
cout << s.filter( [](int i) { return i % 2 == 0; } ) << endl;
int x1, x2;
cin >> x1 >> x2;
cout << s.filter( [x1,x2](int i) { return x1 <= i && i <= x2; } ) << endl;
}
פונקציית למדה מוגדרת כך:
[<capture-list>] (parameters) -> <return-type> { <code> }
זה יוצר פונקציה זמנית ללא שם.
אם הלמדה מכילה רק שורת החזרה, סוג ההחזרה לא נדרש.
cout << s.filter( [x1,x2](int i) -> bool { return x1 <= i && i <= x2; } ) << endl; // not needed
cout << s.filter( [x1,x2](int i) { return x1 <= i && i <= x2; } ) << endl; // great
כדי להשתמש במשתנים מקומיים של הפונקציה שבה אנו יוצרים את הלמדה, נשים אותם ב-. בעצם, אנו לוכדים אותם מהסביבה של למדה.
ניתן ללכוד משתנה גם כרפרנס או פויינטר ולשנות אותו כך:
int sum = 0;
std::for_each(s.begin(), s.end(), [&sum](int n) { sum += n; });
את זה צריך לעשות בזהירות שכן אנו צריכים להיות בטוחים ש- קיים כאשר הלמדה נקראת באמת.
ניתן לתת למהדר להתאמץ ולמצוא את מה שהוא צריך באמצעות כך:
double a, b, c;
// ...
double result = integrate(0.0, 1.0, [=] (double x) { return a*x*x + b*x + c; });
result = integrate(0.0, 1.0, [&] (double x) { return a*x*x + b*x + c; });
זה מאפשר לנו לשלוח לפונקציה את כל מה שאנחנו צריכים מבלי לדעת מה אנחנו צריכים.
כל המשתנים ב- מועברים וב- כל המשתנים מועברים .
בפונקציות למדה בתוך פונקציות מחלקה, ניתן לתפוס את שדות המחלקה.
class Polynomial
{
double a, b, c;
public:
doubel integrate(double xmin, double xmax) const
{
return numerics::integrate(xmin, xmax,
[this] (double x) { return a*x*x + b*x + c; }
);
}
};