Menu

pair ו-tuple ב-C++: קיבוץ ערכים בלי struct

איך std::pair ו-std::tuple אורזים שניים או יותר ערכים לאובייקט אחד: יצירה, גישה לשדות, structured bindings, ואיפה כל אחד מהם מתאים.

בדף הזה יש עורכים שאפשר להריץ - לערוך, להריץ ולראות את הפלט מיד.

שני ערכים, אובייקט אחד

לפעמים צריך להחזיק שני דברים ביחד: שם וניקוד, x ו-y, דגל "האם זה עבד" והתוצאה. אפשר להגדיר struct לכל צמד כזה, אבל לקיבוצים חד-פעמיים זה הרבה טקס. std::pair (מ-<utility>) אורז בדיוק שני ערכים לאובייקט אחד, ו-std::tuple (מ-<tuple>) מכליל את זה לכל מספר קבוע של ערכים.

כבר פגשתם את std::pair בעקיפין בדרך לכאן: כל איבר של std::map הוא pair<const Key, Value>. העמוד הזה הופך את זה למפורש ומראה את הדרכים המודרניות והקריאות לבנות ולפרק את הטיפוסים האלה.

שני האיברים נקראים תמיד .first ו-.second: הם לא מקבלים את שמות המשתנים שלכם. זה המחיר של טיפוס גנרי: שמות השדות מבוססי מיקום, לא תיאוריים.

יצירת זוגות

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

make_pair מסיק בשבילכם את טיפוסי האיברים, וזה היה שימושי לפני C++17. היום אתחול בסוגריים מסולסלים יחד עם הסקת ארגומנטים של תבנית מחלקה (pair p{"Boris", 85};) מכסים את רוב המקרים, אבל עדיין תראו את make_pair בכל מקום בקוד קיים.

מלכודת אחת בהסקה: make_pair("hi", 3) מסיק pair<const char*, int>, לא pair<string, int>. ליטרלים של מחרוזת אינם std::string. אם צריך string, כתבו את זה במפורש, make_pair(string("hi"), 3) או כתיבה מלאה של טיפוס ה-pair, אחרת אתם עלולים לקבל בהמשך השוואות או העתקות מפתיעות.

פירוק עם structured bindings

קריאה של .first ו-.second בכל מקום הופכת מהר ללא קריאה, כי השמות לא אומרים כלום. structured bindings של C++17 מאפשרים לתת לשני השדות שמות אמיתיים בשורה אחת:

זה זוהר בלולאת for מבוססת טווח על map, שבו כל איבר הוא pair. במקום it->first / it->second, נותנים שם ישירות למפתח ולערך:

השתמשו ב-const auto& בלולאה, בדיוק כמו לכל איבר של מכולה: זה חוסך העתקה של כל pair ומסמן שאתם רק קוראים. הסירו את ה-& ותעתיקו כל רשומה, וזה באג ביצועים שקט ב-map גדול.

כששניים לא מספיקים: tuple

pair נעצר בשני ערכים. כשצריך שלושה או יותר, std::tuple הוא אותו רעיון עם מספר שרירותי. בונים אחד עם אתחול בסוגריים מסולסלים או עם make_tuple, וקוראים אותו עם std::get<N>, כאשר N הוא אינדקס שידוע בזמן הקימפול.

האינדקס בתוך get<> חייב להיות קבוע שידוע בזמן הקימפול. get<i>(record) כאשר i הוא משתנה בזמן ריצה לא יתקמפל: לשדות של tuple יכולים להיות טיפוסים שונים, ולכן טיפוס האיבר צריך להיקבע במהלך הקימפול, לא בזמן הריצה. אם אתם מוצאים את עצמכם רוצים אינדקס בזמן ריצה, כנראה שאתם צריכים vector.

structured bindings עובדים גם על tuples, וזו הדרך הקריאה להשתמש בהם:

החזרת כמה ערכים

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

לשלוש תוצאות או יותר, החזירו tuple באותה דרך. יש גם את std::tie, טריק ישן יותר שמפרק לתוך משתנים קיימים במקום להצהיר על חדשים, ושימושי כשרוצים להתעלם משדה עם std::ignore:

אבל מתי לעצור: אם אותה קבוצת שדות מופיעה ביותר ממקום אחד, או שאתם שוכחים כל הזמן אם .second הוא הניקוד או הספירה, הגדירו struct עם איברים בעלי שמות. pair ו-tuple מתאימים ביותר לקיבוצים מקומיים וקצרי חיים, ושדות עם שמות מנצחים ברגע שהנתונים חיים יותר מביטוי אחד.

השוואה ומיון

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

שימו לב שסדר השדות חשוב: שימת age ראשון ממיינת קודם כול לפי גיל, ואז לפי שם כהכרעה בשוויון. אם רציתם סדר לפי שם קודם, הייתם מחליפים את סדר האיברים ב-tuple. ההשוואה ברירת המחדל הזו היא בדיוק הסיבה ש-pair<priority, item> הוא idiom נפוץ לתורי עדיפויות.

הבא בתור: איטרטורים

ראיתם עכשיו את .first, .second, it->first ו-*it מופיעים סביב מכולות: מה שבאמת מחבר איבר pair ל-map שבו הוא נמצא הוא איטרטור. העמוד הבא מפרק איטרטורים כמו שצריך: מה begin() ו-end() באמת מחזירים, איך ++it עובר על מכולה, ומלכודות ביטול האיטרטורים שגורמות לכמה מההתנהגויות הלא מוגדרות הגרועות ביותר ב-C++.

שאלות נפוצות

מה ההבדל בין pair ל-tuple ב-C++?

std::pair מחזיק בדיוק שני ערכים, שניגשים אליהם עם .first ו-.second. std::tuple מחזיק כל מספר קבוע של ערכים (אפס, שניים, שלושה או יותר), שניגשים אליהם עם std::get<N>(t). pair הוא בעצם tuple של שני איברים עם שמות איברים ידידותיים יותר: השתמשו ב-tuple רק כשצריך שלושה שדות או יותר.

איך ניגשים לאיברים של tuple ב-C++?

השתמשו ב-std::get<N>(t) עם אינדקס שידוע בזמן הקימפול, למשל std::get<0>(t). מ-C++17 אפשר גם לפרק עם structured bindings: auto [a, b, c] = t; נותן לכל איבר משתנה עם שם משלו. אי אפשר לגשת ל-tuple עם אינדקס שהוא משתנה בזמן ריצה: std::get<i> דורש i קבוע.

איך מחזירים כמה ערכים מפונקציה ב-C++?

החזירו std::pair או std::tuple ופרקו אותו בנקודת הקריאה עם structured bindings: auto [ok, value] = parse(text);. זה נקי יותר מפרמטרי פלט וחוסך הגדרה של struct חד-פעמי, אם כי struct עם שמות קריא יותר כשהשדות חיים יותר מקריאה אחת.

איור של שפות התכנות ב-Coddy

ללמוד תכנות עם Coddy

להתחיל