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 עוצמתי במיוחד ליצירת mixins — פונקציונליות לשימוש חוזר שאפשר "לערבב" בתוך מחלקות. בניגוד לירושה מסורתית, mixins מוסיפים יכולות בלי ליצור היררכיות עמוקות:
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היתרון המרכזי הוא שכל הקריאות למתודות מוכרעות בזמן הידור, וכך נמנעת התקורה של פונקציות וירטואליות. אפשר לשלב כמה mixins של 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 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++ אונליין