Menu
flag Ar iconالعربيةdown icon

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

هناك ثلاث طرق شائعة لإنشاء 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 ويشير إلى أنك تقرأ فقط. أزِل & فتنسخ كل مدخلة؛ وهذا خطأ أداء صامت على خريطة كبيرة.

حين لا يكفي اثنان: tuple

يتوقف pair عند قيمتين. فعندما تحتاج إلى ثلاث قيم أو أكثر، يكون std::tuple الفكرة نفسها بعدد اعتباطي. تبنيه بالتهيئة بالأقواس المعقوفة أو بـmake_tuple، وتقرؤه بـstd::get<N>، حيث N فهرس معروف وقت الترجمة.

يجب أن يكون الفهرس داخل get<> ثابتًا معروفًا وقت الترجمة. والتعبير get<i>(record) حيث i متغيّر يُحسب وقت التشغيل لن يُترجَم: فحقول الـtuple قد تكون من أنواع مختلفة، لذا يجب تحديد نوع العنصر أثناء الترجمة لا وقت التشغيل. وإن وجدت نفسك ترغب في فهرس يُحسب وقت التشغيل، فالأرجح أن ما تريده هو vector.

تعمل structured bindings مع الـtuple أيضًا، وهي الطريقة الواضحة لاستهلاكه:

إرجاع عدة قيم

السبب اليومي للجوء إلى هذين النوعين هو إرجاع أكثر من قيمة من دالة دون اختراع struct أو التلاعب بمعاملات الإخراج. اجمع النتائج في pair أو tuple وفكّكها عند موضع الاستدعاء.

ولثلاث نتائج أو أكثر، أرجِع tuple بالطريقة نفسها. وهناك أيضًا std::tie، وهي حيلة أقدم تفكّك إلى متغيّرات موجودة مسبقًا بدلًا من التصريح بمتغيّرات جديدة، وهي مفيدة عندما تريد تجاهل حقل عبر std::ignore:

لكن اعرف متى تتوقف: إن ظهرت المجموعة نفسها من الحقول في أكثر من موضع، أو ظللت تنسى هل .second هو الدرجة أم العدد، فعرّف struct بأعضاء مسمّاة. فالـpair والـtuple أنسب للتجميعات المحلية القصيرة العمر؛ وتتفوق الحقول المسمّاة فور أن تبقى البيانات حيّة أطول من تعبير واحد.

المقارنة والترتيب

ميزة إضافية مفيدة: يأتي pair وtuple بمعاملات مقارنة مدمجة تعمل معجميًّا (lexicographically)؛ فهي تقارن العنصر الأول ولا تنتقل إلى التالي إلا عند تساوي الأولَين. وهذا يجعلهما مفاتيح ترتيب مثالية.

لاحظ أن ترتيب الحقول مهم: وضع age أولًا يرتّب حسب العمر بالدرجة الأولى، ثم حسب الاسم لفضّ التعادل. ولو أردت ترتيبًا يبدأ بالاسم، لبدّلت ترتيب عناصر الـtuple. وهذه المقارنة الافتراضية هي تحديدًا السبب الذي يجعل pair<priority, item> أسلوبًا شائعًا لطوابير الأولوية.

التالي: المُكرِّرات (Iterators)

رأيت الآن .first و.second وit->first و*it تظهر حول الحاويات؛ والشيء الذي يربط فعليًا عنصر pair بالـmap التي يعيش فيها هو المُكرِّر (iterator). تشرح الصفحة التالية المُكرِّرات كما ينبغي: ما الذي يُرجِعه فعلًا كل من 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 programming languages illustration

تعلّم البرمجة مع Coddy

ابدأ الآن