Mixins באמצעות CRTP
חלק מהיחידה תכנות מונחה עצמים במסלול ה-C++ של Coddy. שיעור 77 מתוך 104.
תבנית התבנית החוזרת באופן מסקרן (CRTP) היא טכניקה שבה מחלקה יורשת ממחלקת בסיס תבניתית, ומעבירה את עצמה כארגומנט לתבנית. כך מתאפשר פולימורפיזם בזמן הידור, ומחלקות בסיס יכולות לגשת לחברי המחלקה הנגזרת ללא פונקציות וירטואליות.
המבנה הבסיסי של CRTP נראה כך:
template <typename Derived>
class Base {
public:
void interface() {
static_cast<Derived*>(this)->implementation();
}
};
class MyClass : public Base<MyClass> {
public:
void implementation() {
std::cout << "MyClass implementation\n";
}
};CRTP עוצמתי במיוחד ליצירת מיקסינים — פונקציונליות רב־פעמית שאפשר "לערבב לתוך" מחלקות. בניגוד לירושה מסורתית, מיקסינים מוסיפים יכולות בלי ליצור היררכיות עמוקות:
template <typename Derived>
class Printable {
public:
void print() const {
const Derived& self = static_cast<const Derived&>(*this);
std::cout << self.toString() << "\n";
}
};
class Person : public Printable<Person> {
std::string name;
public:
Person(const std::string& n) : name(n) {}
std::string toString() const { return "Person: " + name; }
};
// שימוש:
Person p("Alice");
p.print(); // פלט: Person: Aliceהיתרון המרכזי הוא שכל הקריאות למתודות מוכרעות בזמן הקומפילציה, וכך נמנעת התקורה של פונקציות וירטואליות. אפשר לשלב כמה מיקסינים של CRTP כדי להרכיב פונקציונליות, וכך ליצור חלופה גמישה לפולימורפיזם בזמן ריצה כשהטיפוסים ידועים בזמן הקומפילציה.
אתגר
קלנבנה מערכת רישום שמשתמשת במיקסינים של CRTP כדי להוסיף פונקציונליות לשימוש חוזר למחלקות שונות, בלי העלות של פונקציות וירטואליות. תיצור שני מיקסינים שאפשר "לשלב" בכל מחלקה: אחד לספירת מופעים ואחד ליצירת ייצוגי מחרוזת.
תארגן את הקוד שלך בשלושה קבצים:
Mixins.h: הגדר שתי תבניות מיקסין של CRTP שמספקות פונקציונליות לשימוש חוזר.צור תבנית מיקסין בשם
Countableשעוקבת אחר מספר המופעים של מחלקה נגזרת. צריכה להיות לה מונה סטטי שגדל בבנאי וקטן בהורס. ספק מתודה סטטיתgetCount()שמחזירה את הספירה הנוכחית.צור תבנית מיקסין בשם
Describableשמספקת מתודה בשםdescribe(). המתודה הזאת צריכה להשתמש ב-static_castכדי לגשת למחלקה הנגזרת ולקרוא למתודה שלה,getDescription(), ואז להדפיס את התוצאה ואחריה שורה חדשה.זכור שמיקסינים של CRTP משתמשים ב-
static_cast<Derived*>(this)כדי לגשת לאיברים של המחלקה הנגזרת בזמן קומפילציה.Entities.h: הגדר שתי מחלקות ישויות שיורשות משני המיקסינים.צור מחלקה בשם
Playerשיורשת גם מ-Countable<Player>וגם מ-Describable<Player>. עליה לאחסן שם ורמה, ולספק מתודה בשםgetDescription()שמחזירה מחרוזת בתבנית:Player: [name] (Level [level])צור מחלקה בשם
Enemyשיורשת גם היא משני המיקסינים. עליה לאחסן סוג ובריאות, ולספק מתודה בשםgetDescription()שמחזירה:Enemy: [type] with [health] HPאל תשכח לאתחל את המונה הסטטי עבור כל מחלקה.
main.cpp: קרא ארבעה קלטים (כל אחד בשורה נפרדת):- שם השחקן (מחרוזת)
- רמת השחקן (מספר שלם)
- סוג האויב (מחרוזת)
- הבריאות של האויב (מספר שלם)
צור Player ו-Enemy עם הערכים שניתנו. לאחר מכן הדגם את המיקסינים:
- הדפס
Player count: [count]באמצעות המתודה הסטטיתgetCount() - הדפס
Enemy count: [count] - קרא ל-
describe()עבור השחקן - קרא ל-
describe()עבור האויב - צור שחקן שני עם השם "Guest" ורמה 1
- הדפס שוב
Player count: [count]כדי להציג את הספירה המעודכנת - קרא ל-
describe()עבור השחקן השני
לדוגמה, עם הקלטים Hero, 10, Dragon ו-500:
Player count: 1
Enemy count: 1
Player: Hero (Level 10)
Enemy: Dragon with 500 HP
Player count: 2
Player: Guest (Level 1)האתגר הזה מדגים כיצד מיקסינים של CRTP מוסיפים פונקציונליות (ספירה ותיאור) למחלקות שאינן קשורות זו לזו בלי להשתמש בפונקציות וירטואליות. גם Player וגם Enemy מקבלות את אותן יכולות באמצעות ירושה מאותן תבניות מיקסין, אך כל אחת מנהלת מונה מופעים נפרד משלה, משום שהתבנית עוברת התממשות עם טיפוסים שונים.
נסו בעצמכם
#include <iostream>
#include <string>
#include "Entities.h"
using namespace std;
int main() {
// קראו את הקלטים
string playerName;
int playerLevel;
string enemyType;
int enemyHealth;
cin >> playerName;
cin >> playerLevel;
cin >> enemyType;
cin >> enemyHealth;
// TODO: צרו Player עם השם והרמה הנתונים
// TODO: צרו Enemy עם הסוג והבריאות הנתונים
// TODO: הדפיסו "Player count: [count]" באמצעות Player::getCount()
// TODO: הדפיסו "Enemy count: [count]" באמצעות Enemy::getCount()
// TODO: קראו ל־describe() עבור השחקן
// TODO: קראו ל־describe() עבור האויב
// TODO: צרו שחקן שני בשם "Guest" וברמה 1
// TODO: הדפיסו שוב "Player count: [count]"
// TODO: קראו ל־describe() עבור השחקן השני
return 0;
}
השיעור הזה כולל חידון קצר. התחילו את השיעור כדי לענות עליו ולעקוב אחרי ההתקדמות.
כל השיעורים ביחידה תכנות מונחה עצמים
1יסודות OOP
קבצים חיצונייםבנייה וקומפילציה של C++קובצי Header וקובצי מקורמרחבי שמות והיקףמבוא ל-OOP ב-C++מחלקות לעומת אובייקטיםמצביע thisמתודות (פונקציות איבר)תכונות (חברי נתונים)יסודות הבנאים וההורסיםסיכום – מחשבון פשוט4מאפייני מחלקה
חברי מופע לעומת חברים סטטייםגטרים וסטריםפונקציות איבר קבועותמילת המפתח mutableמתודות ומשתנים סטטייםפונקציות ומחלקות ידידותסיכום – מנהל חשבונות בנק7ירושה
ירושה בסיסיתרמות גישה בירושהסדר הקריאות לבנאי ולמפרקדריסת מתודותפונקציות וירטואליות ו־VTableירושה מרובהירושה וירטואליתסיכום – היררכיית עובדים2ניהול זיכרון
זיכרון Stack לעומת Heapמצביעים והפניותזיכרון דינמי (new/delete)מצביעים חכמים ב-C++RAII ב-C++סיכום – מנהל מערכים דינמיים5כימוס
מצייני גישה ב-C++מצייני גישה לעומקהסתרת מידעמבנה לעומת מחלקהמחלקות מקוננות ופנימיותסיכום – מערכת רשומות תלמידים8רב־צורתיות
רב־צורתיות בזמן קומפילציה לעומת בזמן ריצההעמסת פונקציותפונקציות וירטואליות — מבט נוסףפונקציות וירטואליות טהורותמחלקות מופשטותתכנון ממשקים ב־C++המרה דינמית ו־RTTIסיכום — מחשבון צורות11מושגי OOP מתקדמים
הרכבה לעומת הורשהMixins באמצעות CRTPאידיום Pimplמחיקת טיפוסיםמחלקות Enum וטיפוסיות חזקהטיפול בחריגות ב-OOPהיררכיות חריגות מותאמות אישית3בנאים והורסים
בנאי ברירת מחדלבנאי עם פרמטריםבנאי העתקהבנאי העברהרשימות אתחול בבנאיםבנאים מאציליםמעמיקים בנושא הורסיםכלל השלושה / החמישה / האפססיכום – מחלקת String6העמסת אופרטורים
מבוא להעמסת אופרטוריםהעמסת אופרטורים אריתמטייםהעמסת אופרטורי השוואהאופרטורי זרםהעמסת אופרטור השמההעמסת האופרטורים [] ו־()אופרטורים להמרת טיפוסיםסיכום – מחלקת Matrix9תבניות
תבניות פונקציהתבניות מחלקההתמחות תבניותתבניות ארגומנטים משתנותיסודות SFINAE ותכונות טיפוסיםסיכום – מכל גנריתרגלו בעצמכם: קומפיילר C++ אונליין