Menu
Coddy logo textTech

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

challenge icon

אתגר

קל

נבנה מערכת רישום שמשתמשת במיקסינים של 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: קרא ארבעה קלטים (כל אחד בשורה נפרדת):
    1. שם השחקן (מחרוזת)
    2. רמת השחקן (מספר שלם)
    3. סוג האויב (מחרוזת)
    4. הבריאות של האויב (מספר שלם)

    צור Player ו-Enemy עם הערכים שניתנו. לאחר מכן הדגם את המיקסינים:

    1. הדפס Player count: [count] באמצעות המתודה הסטטית getCount()
    2. הדפס Enemy count: [count]
    3. קרא ל-describe() עבור השחקן
    4. קרא ל-describe() עבור האויב
    5. צור שחקן שני עם השם "Guest" ורמה 1
    6. הדפס שוב Player count: [count] כדי להציג את הספירה המעודכנת
    7. קרא ל-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;
}
quiz iconבחנו את עצמכם

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

כל השיעורים ביחידה תכנות מונחה עצמים

תרגלו בעצמכם: קומפיילר C++ אונליין