בדיקת טיפוסים בזמן ריצה ב-C++ - עמוד 3 - תכנות - HWzone פורומים
עבור לתוכן
  • צור חשבון

בדיקת טיפוסים בזמן ריצה ב-C++


MiniMizer

Recommended Posts

אתה צודק, קו המחשבה המנחה את התרגיל (ואת כל הקורס למעשה) הוא גנריות - לכתוב קוד גנרי עד כמה שניתן, על מנת לאפשר גמישות מירבית בעתיד, Cosmic Hierarchy אם תרצה.

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

קישור לתוכן
שתף באתרים אחרים

  • תגובות 45
  • נוצר
  • תגובה אחרונה

עדיף שתקרא על זה בצורה רצינית יותר, אבל הנה תקציר.

דרך אחת לחלק את שפות התכנות היא לפי ההתייחסות לטיפוסים: בגדול אפשר לחלק שפות לשני סוגים: dynamically typed או statically typed.

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

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

חשוב מאוד להדגיש שלכל אחת מהשיטות חסרונות ויתרונות משלה, אבל היום אנחנו מדברים על C++.

מערכת הטיפוסים של C++ היא סטטית והיא חזקה (strong typing, אם כי יש שפות יותר קפתדניות). פירוש הדבר זה שבזמן קומפילציה (וגם לרוב בזמן link) הקומפיילר יודע (בגדול) את הטיפוס של כל משתנה, כל פוקנציה, כל פרמטר וכל ערך, והוא אוכף זאת. היתרונות הן זיהוי שגיאות כבר בזמן קומפילציה, וכן ביצועים יותר טובים.

זיהוי שגיאות: סוגים מסויימים של שגיאות לא יכולים לקרות. אתה לא יכול להציב אובייקט מסוג A לתוך משתנה מסוג B אם הם לא קשורים. יש לכך השלכות רחוקות: נניח שמימשת אובייקט שמכיל עטים (PenHolder). ב-C++ רשימה זו תכיל רק מצביעים לאובייקטים מסוג Pen או ליורשים ממנה. כל נסיון להכניס משהו אחר יגרום לשגיאת קומפילציה.

אולם כאשר משתמשים ב-cosmic hierarchies (כלומר הרשימה מכילה מצביעים ל-Object שממנו כולם יורשים), אז אפשר להכניס כל סוג של אובייקט לרשימה! אפילו אם הוא ספינת מלחמה! (Battleship). זה כמובן בעיה. עכשיו כאשר עוברים על הרשימה צריך לוודא שכל האובייקטים הן Pen ולא משהו אחר.

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

קישור לתוכן
שתף באתרים אחרים

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

גם אם יום אחד יבוא מישהו וייצור אובייקט Battleship שיורש מ-Dish, הוא יהיה Dish לכל דבר (כי Dish נותן לבנים שלו שדות ופונקציות ספציפיות שיש רק לכלי מטבח ולא לספינות מלחמה..).

קישור לתוכן
שתף באתרים אחרים

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

גם אם יום אחד יבוא מישהו וייצור אובייקט Battleship שיורש מ-Dish, הוא יהיה Dish לכל דבר (כי Dish נותן לבנים שלו שדות ופונקציות ספציפיות שיש רק לכלי מטבח ולא לספינות מלחמה..).

אתה מתבלבל, אולי כי הדגשתי את הדבר הלא נכון.

הבעיתיות של מה שאני רואה פה זה לא בגלל שהוא ,cosmic hierarchy, כי הוא לא (אם כי נראה לי שאתם צועדים בכיוון, אילו זה היה תוכנה אמיתית ולא תרגיל). הבעיתיות היא השאלה המפורשת שאתה שואל אובייקט: מה הטיפוס שלך? האם אתה מזלג? האם אתה צלחת? וכו'. זו הבעיתיות שמשותפת גם לקוד כמו שאתה מדבר עליו, וגם ל-cosmic hierarchies.

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

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

אולי זה מופשט מדי ולכן אפרש עם דוגמא (של Scott Meyers):


class Animal { ... };
class Tiger : public Animal { ... };
class Wombat : public Animal { ... };
class Narwhal : public Animal { ... };

void Nap(Animal& a)
{
if (Tiger* pt = dymanic_cast<Tiger*>(&a) ) pt->Sleep(7);
else
if (Wombat* pw = dymanic_cast<Wombat*>(&a) ) pw->Sleep(12);
else
if (Narwhal* pn = dymanic_cast<Narwhal*>(&a) ) pn->Sleep(8);
else
throw std::exception( "Unknown Animal");
}

לקוד הזה כבר יש הרבה בעיות, ואפילו לא התחלנו:

1) הוא עלול להיכשל! יש לו failure mode מיותר לחלוטין.

2) על פניו קצת קשה לתחזק אותו - הוא ארוך ופחות קריא מקוד אלטרנטיבי (ראה למטה).

3) מה אם יש 30 טיפוסי חיות, במקום 3?

4) הוא קצת איטי יותר ממה שהוא חייב להיות.

אבל הבעיות הגדולות של השיטה מתחילות עכשיו! מה קורה אם מישהו מוסיף סוג חדש של חיה? הסיכוי שאותו מישהו יזכור לעדכן את הפונקציה Nap הוא נמוך: זה יכול להיות מישהו אחר בצוות שלך, מישהו אחר בצוות אחר בפרויקט, או אפילו לקוח שקיבל את הקוד שלך ויש לו את ה-headers אבל לא את ה-source.

מזל טוב! הרגע יצרת באג חמור בקוד!

עוד בעיה היא שכל קוד דומה צריך גם הוא להתחיל לבדוק את הטיפוסים. כלומר צריך לכתוב קוד "מה הטיפוס שלך" דומה עבור כל פעולה כמו Feed, Play.

הבה נשווה את זה לקוד אלטרנטיבי:


class Animal {
public:
virtual void Nap() = 0;
virtual void Eat() = 0;
virtual void Play() = 0;
};

class Tiger : public Animal { ... };
class Wombat : public Animal { ... };
class Narwhal : public Animal { ... };


void Nap(Animal& a)
{
a.Nap();
}

void Eat(Animal& a)
{
a.Eat();
}

void Play(Animal& a)
{
a.Play();
}

אני ממליץ בחום לכל מי שקצת חשוב לו להיות מתכנת סביר ב-++C לקרוא את ++Effective C ואת ++More Effective C של Scott Meyers.

ולמתכנת ++C שזה שזה לא חשוב לו, אני ממליץ בחום שיקפוץ ערום לבריכה מלאה תנינים, או שיתחיל להתעניין בנושא.

(ההוראות ב-post זה נכתבו בלשון זכר, אך הן מיועדות גם לנקבות ולהרמפרודיטים).

קישור לתוכן
שתף באתרים אחרים

נכון, הבנתי את הבעיה, לשאול כל אובייקט "האם אתה X? האם אתה Y? האם אתה Z?" זו לא דרך נכונה.

אם כך, *מהי* הדרך הנכונה ביותר להשוות בין סוגי טיפוסים בזמן ריצה, אם כל מה שאני צריך לקבל בחזרה זה ערך TRUE/FALSE ?

קישור לתוכן
שתף באתרים אחרים

נכון, הבנתי את הבעיה, לשאול כל אובייקט "האם אתה X? האם אתה Y? האם אתה Z?" זו לא דרך נכונה.

אם כך, *מהי* הדרך הנכונה ביותר להשוות בין סוגי טיפוסים בזמן ריצה, אם כל מה שאני צריך לקבל בחזרה זה ערך TRUE/FALSE ?

(תקרא את ה-post המעודכן)

הדרך ה"נכונה" (ותמיד יש מקרים שהיא דווקא לא נכונה) היא לתכנן את התוכנה ככה שלא צריך לשאול את השאלה הזו.

אם אתה ממש חייב לשאול אותה, בד"כ שאלה פשוטה על טיפוס ספציפי (כלומר כן/לא) עדיפה:


// Bad
void FlyMeToTheMoon1(Animal& a)
{
if (Wombat* pw = dymanic_cast<Wombat*>(&a) ) pw->Fly( TO_THE_MOON );
else
if (Parrot* ppa = dymanic_cast<Parrot*>(&a) ) ppa->Fly( TO_THE_MOON );
else
if (Pterodactyl* ppt = dymanic_cast<Pterodactyl*>(&a) ) ppt->Fly( TO_THE_MOON );
else
throw std::Exception("Can't fly");
}

// Better
void FlyMeToTheMoon2(Animal& a)
{
if (FlyingAnimal* pf = dymanic_cast<FlyingAnimal*>(&a) ) pf->Fly( TO_THE_MOON );
else
throw std::Exception("Can't fly");
}

קישור לתוכן
שתף באתרים אחרים

אוקי, הבנתי, תודה רבה :)

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

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

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

איך אפשר לפתור את הבעיה הזאת? הדרך היחידה שחשבתי עליה היא לבנות מערכת של if-ים (למשל, אם האובייקט שלי הוא מטיפוס בן א', אז תקרא לפונקציה שמקבלת פוינטר לבן א' וכו'), אבל זה פתרון ממש גרוע וחייב להיות פתרון אלגנטי יותר.

קישור לתוכן
שתף באתרים אחרים

נכון. זה לא אלגנטי, ואני רואה שאתה מפתח אינטואיציה בריאה!

כתיבת operator== בנוכחות פולימורפיזם זו בעיה ידועה, גם ב-JAVA. יש כל מני פתרונות. תלוי איזה סוג של השוואה אתה מחפש.

מאחר שאתה רוצה גם לוודא את הטיפוס, typeid יעזור לך ( http://msdn.microsoft.com/en-us/library/fyf39xec(VS.80).aspx ).

מזמן לא עשיתי את זה, אבל הייתי עושה משהו כמו שילוב של הרעיון הזה: http://stackoverflow.com/questions/565765/implementing-operator-when-using-inheritance/565999#565999

יחד עם הרעיון הזה: http://stackoverflow.com/questions/565765/implementing-operator-when-using-inheritance/565784#565784


Class Animal
{
public:
bool operator==(const Animal& rhs) const;
protected:
virtual bool implementIsEqual(const Animal& a) const = 0;
};
bool operator==(const Animal& rhs) const
{
if ( typeid(this) != typeid(&rhs) ) return false; // different concrete types
return implementIsEqual(rhs);
}

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

עריכה: עוד גישה : http://artis.imag.fr/~Xavier.Decoret/resources/C++/operator==.html

קישור לתוכן
שתף באתרים אחרים

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

שנית כל, היא pure virtual, משמע בכל בן אני אצטרך לממש אותה באופן שונה - וכאן אני שוב מגיע לאותה נקודה שבה אני תקוע; אם נמשיך עם הדוגמה שלך:

class Animal
{

string color;

}

class Leopard: public Animal
{

bool carnivore;

}

class Cow: public Animal
{

int weight;

}

ב-Lion הפונקציה implementIsEqual תקבל פוינטר ל-Animal תדרש לבדוק האם האובייקט שקיבלתי הוא גם Carnivore וב-Cow הפונקציה תקבל פוינטר ל-Animal תדרש לבדוק האם המשקל זהה... אבל זה פשוט לא יתקמפל, כי ב-Animal אין שדות של Carnivore ו-weight :-\

קישור לתוכן
שתף באתרים אחרים

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

אם הטיפוס לא תואם, היא תחזיר ישר false ונגמר הסיפור.

אם הטיפוס תואם, היא תעביר את הפוינטר הלאה לפונקציה פנימית בתור פוינטר של בן, והפונקציה הפנימית כבר תבצע את ההשוואה.

בוא נראה אם זה יעבוד :)

קישור לתוכן
שתף באתרים אחרים

כך נראה הקוד שלי (רעיונית):

bool Main_Compare(Father * object) {
if (type == object.type) {
return Specific_Compare(object);
else return false;
}

bool Specific_Compare(Son * object) {
if (...) return true;
else return false;
}

הבעיה שלי (של הקומפיילר ליתר דיוק) היא בהמרה של הפוינטרים, הוא צועק שזה invalid conversion (ואני נאלץ להאמין לו).

אז קודם כל, האם פתרון כזה יעבוד ברמה התאורטית? ואם כן, איך אני צריך לבצע את ההמרה?

\\עריכה: התגברתי על ההמרה באמצעות dynamic_cast. עודנה נשאלת השאלה האם קונספט זה יעבוד?

קישור לתוכן
שתף באתרים אחרים

אני לא בטוח לאן אתה הולך עם כל זה, ואין לי הרבה זמן לברר, אבל:

א) אם יש לך פוינטר ל-X ואתה רוצה רפרנס, פשוט תעשה *

ב) זה ברור שלכל בן צריך לממש את implementIsEqual. זה כל הרעיון! יתכן שגם תצטרך לממש מחדש את operator== לכל בן, אני לא מאה אחוז בטוח.

ג) המרה של אבא לבן היא המרה מסוכנת שבעקרון חייבת להיעשות עם dynamic_cast, ולכן הקומפיילר צועק. אחת המטרות העיקריות של מה שכתבתי כאן היא להמנע מכך!

ד) בקוד שלך ב-Main_Compare אתה שוב גורם לקוד של האבא להכיר את הבן (Son מופיע בחתימה של Specific_Compare).

אם תחשוב על זה, אתה תראה שהקוד שהצעתי למעלה עושה מה שאתה מנסה לעשות אבל בצורה בטוחה יותר:

1) המשתמש קורא ל-Animal::operator== (שזה אצלך כמו Main_Compare). י

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

3) האב קורא לפונקציית ההשוואה המתאימה לבן פשוט ע"י קריאה לפונקציה וירטואלית! (שזה אצלך כמו בדיקת ה-type וקריאה ל-Specific_Compare).

קישור לתוכן
שתף באתרים אחרים

בהצלחה.

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

קישור לתוכן
שתף באתרים אחרים

ארכיון

דיון זה הועבר לארכיון ולא ניתן להוסיף בו תגובות חדשות.


×
  • צור חדש...