חזור לתיקייה: 02340124 - מבוא לתכנות מערכות

4 - תכנות גנרי

חיפוש בתיקיה
Ctrl K
לחץ כאן או הקש Ctrl K כדי לחפש בכל התיקיה
  • כללי
  • מחלקות גנריות
  • אופרטור חץ
  • הגבלות על T
  • מחלקות שהן פונקציות
  • פונקציות למדה

4 - תכנות גנרי

02340124

מבוא לתכנות מערכות

רשימות הרצאה לקריאה בשבת

תכנות גנרי

הופק מתוך תקיית התואר של שקד

uni-folder.vercel.app

כשאנו כותבים מחלקה, ראינו כי אנו חייבים לקבוע לה טיפוסים לשדות.
ראינו גם כי בנינו SetSetSet של intintint. אבל מה אם נרצה SetSetSet עבור doubledoubledouble או PersonPersonPerson או כל דבר אחר? אנו לא רוצים להעתיק את הקוד מלאא פעמים.
לשם כך, C++C++C++ נותנת לנו את האפשרות להגדיר שבלונה של מחלקה אשר בה נוכל להשתמש בטיפוס אשר לו נקרא בשם מסויים ואז כאשר אנו יוצרים את הקוד אנו נוכל להגיד תקרא ל-SetSetSet של intintint או doubledoubledouble או כל דבר אחר.

דוגמה (פונקציה גנרית)

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;
}

ניתן לחשוב על מה שקורה פה כהמהדר יוצר מלא ההעמסות עבור הפונקציה maxmaxmax.

הערה

איך אנו יודעים כי עבור TTT יש את הפעולה >>>? ובכן, זוהי הבעיה הגדולה עם templatestemplatestemplates כי ל-C++C++C++ לא איכפת ⇐ לא יתקמפל.
("ב-C++23C++23C++23 הוסיפו קונספט בשם קונספט" - גל ללוש (הקונספט מאפשר להגביל את TTT אבל לא נלמד עליו בקרוס הזה או, ככל הנראה, באף קורס בטכניון כרגע))

דוגמה

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];
}

כאשר אנו כתובים מחלקת templatetemplatetemplate - כל הקוד, גם המימוש, צריך להיות בקובץ .h.h.h - זה חשוב מאוד!

הערה

Array< ⁣int ⁣>Array<\!int\!>Array<int> הוא לא אותה מחלקה כמו Array< ⁣double ⁣>Array<\! double\!>Array<double>.
הן מתנהגות כמחלקות שונות לגמרי שאינן קשורות.

נשים לב כי אם ננסה לממש תהיה בעיה אחת: ב-operator=(… )operator=(\dots)operator=(…) אם אנו הורסים את האובייקט שלנו ואז איפשהו בבנאי ההעתקה הוא נכשל, אנחנו בבעיה רצינית. לכן, נצור פונקציה swapswapswap ונשתמש בה כך:

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
}

אופרטור חץ

כשראינו איטרטורים הבנו שהם מייצגים סוג של פויינטר אך לא מימשנו את אחת ההתנהגויות של פויינטר רגיל והיא אופרטור חץ: →\to→ (בהמשך אנו נראה smart  pointerssmart\;pointerssmartpointers ושם נראה למה זה שימושי בכלל)
כעת, פשוט נממש אחד שכזה.

דוגמה

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]);
}

הגבלות על TTT

אם אנחנו לא מניחים שום דבר על TTT אנחנו די לא יכולים לעשות איתו כלום:

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

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

הערה

כאשר אנו לוקחים אובייקט TTT אשר אין לו את כל הפונקציות הדרושות, המהדר פשוט לא ייצר חלק מהפונקציות - אלו שמשתמשות בפונקציות שאין.
הפונקציות שלא משתמשות בפונקציות שאין יווצרו ונוכל להשתמש בהן.

מחלקות שהן פונקציות

הגדרה 1 (אובייקט פונקציה)

מחלקה שמיישמת את האופרטור operator()operator()operator() נקרא אובייקט פונקציה.

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

דוגמה

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;
}

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

דוגמה

לפני כן נראה עוד מחלקת עזר נחמדה:

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;
	}
};

כעת נוכל לבנות לתוך קבוצה filterfilterfilter שכזה שאנו יכולים להפעיל עם כל פונקציה על מנת למיין איברים מהקבוצה.

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;
}

פונקציות למדה

זה מבאס לכתוב מחלקה שלמה כל פעם שאחנו רוצים פונקציה אחת ולכן ב-C++11C++11C++11 הוסיפו את הקונספט בשם lambda  functionlambda\;functionlambdafunction אשר מקצרים לנו את כל הבלגן.
כעת, במקום להגדיר מחלקה בשם iBetweeniBetweeniBetween או DividesByDividesByDividesBy נוכל לכתוב כך:

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;
}
הגדרה 2 (פונקציית למדה)

פונקציית למדה מוגדרת כך:

[<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

כדי להשתמש במשתנים מקומיים של הפונקציה שבה אנו יוצרים את הלמדה, נשים אותם ב-capture  listcapture\;listcapturelist. בעצם, אנו לוכדים אותם מהסביבה של למדה.

הערה

ניתן ללכוד משתנה גם כרפרנס או פויינטר ולשנות אותו כך:

int sum = 0;
std::for_each(s.begin(), s.end(), [&sum](int n) { sum += n; });

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

הערה

ניתן לתת למהדר להתאמץ ולמצוא את מה שהוא צריך באמצעות [=][=][=] כך:

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; });

זה מאפשר לנו לשלוח לפונקציה את כל מה שאנחנו צריכים מבלי לדעת מה אנחנו צריכים.
כל המשתנים ב-=== מועברים by  valueby\; valuebyvalue וב-[&][\&][&] כל המשתנים מועברים by  refrenceby\;refrencebyrefrence.

הערה

בפונקציות למדה בתוך פונקציות מחלקה, ניתן לתפוס את שדות המחלקה.

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; }
		);
	}
};