اجعل أنواعك تبدو وكأنها مدمجة
تعرف بالفعل أن std::string يتيح لك كتابة a + b للدمج و cout << s للطباعة. هذه ليست حيلًا خاصة من المترجم - بل دوال عادية بأسماء غريبة. تحميل المعاملات الزائد هو الميزة التي تتيح لأصنافك أنت أن تتصل بالصياغة نفسها، بحيث يمكن جمع نوع مثل Vector2 أو Money ومقارنته وطباعته تمامًا كما لو كان int.
الآلية بسيطة بمجرد أن تراها: تعبير مثل a + b هو اختصار. يعيد المترجم كتابته على شكل استدعاء لدالة اسمها operator+ ويبحث عن واحدة تطابق أنواع المعاملات التشغيلية. عرّف تلك الدالة لصنفك فيعمل a + b فجأة. هذا في الحقيقة شكل متخصص من تحميل الدوال الزائد - تنطبق هنا قواعد تحليل الأسماء نفسها، لكن بأسماء على هيئة معاملات فقط.
لاحظ أن الدالة تأخذ كلا المعاملين التشغيليين بـ const&: لا ينبغي للحساب أن يعدّل مدخلاته، والمراجع تتجنب النسخ. وهي تُرجع Vector2 جديدًا بالقيمة - يجب أن ينتج p + q نتيجة جديدة دون المساس بـ p أو q، تمامًا كما أن 2 + 3 لا يغيّر القيمة 2.
عضو مقابل غير عضو
هناك مكانان لتعريف معامل: كـ عضو في الصنف أو كـ دالة حرة (غير عضو). كعضو، يكون المعامل التشغيلي الأول هو this الضمني، لذا يأخذ المعامل الثنائي وسيطًا صريحًا واحدًا فقط:
الكلمة const بعد قائمة الوسائط مهمة: يجب ألا يعدّل a + b القيمة a، لذا يُوسَم العضو بـ const. استخدم صيغة العضو للمعاملات المرتبطة جوهريًا بالمعامل التشغيلي الأول والتي لا تحتاج إلى تحويلات عليه - += و [] و () و -> والمعاملات الأحادية مثل -x أو ++x.
المأزق مع الأعضاء: لا يمكن تحويل المعامل التشغيلي الأول. مع العضو operator+ أعلاه، يعمل a + 50 (يُحوَّل 50 إلى Money للجهة الثانية)، لكن 50 + a لا يُترجَم - فالمعامل التشغيلي الأول 50 هو int، ولا يمكنك إضافة دالة عضو إلى int. المعامل غير العضو يصلح هذا لأن كلا المعاملين التشغيليين وسيطان صريحان ويمكن تحويل كليهما:
قاعدة عملية: اجعل المعاملات الثنائية المتماثلة (+ و == و *) غير أعضاء كي تعمل التحويلات على الجهتين؛ واجعل المعاملات التي يجب أن تعدّل المعامل التشغيلي الأول أو المرتبطة به (+= و [] و =) أعضاء.
تحميل معامل التدفق الزائد
المعامل الأكثر شيوعًا للتحميل الزائد بفارق كبير هو << للطباعة. لا يمكنك جعله عضوًا في صنفك، لأن المعامل التشغيلي الأول هو std::ostream (مثل cout)، وليس نوعك - وأنت لا تملك ostream. لذا فهو دائمًا غير عضو يأخذ التدفق بمرجع غير ثابت ويُرجعه:
تفصيلان يجعلان هذا يعمل. يُمرَّر التدفق ويُرجَع بالمرجع (ostream&) - فالتدفقات لا يمكن نسخها، وإرجاع التدفق نفسه هو ما يتيح لك تسلسل cout << "p = " << p << "\n". كل << يُرجع التدفق كي يجد << التالي شيئًا يرتبط به. انسَ return os; فينكسر التسلسل.
معاملات المقارنة
لمقارنة كائناتك بـ == و < وما شابه، حمّل معاملات المقارنة. قبل C++20 كنت تكتب كلًا منها يدويًا؛ والمأزق الأساسي هو أن operator< يجب أن يُرجع bool وأن يحدّد ترتيبًا متسقًا:
كتابة المقارنات الست كلها (== و != و < و <= و > و >=) يدويًا أمر ممل وعرضة للأخطاء. أضافت C++20 معامل المقارنة الثلاثية <=> (معامل «المركبة الفضائية»). جعله افتراضيًا مع == يولّد لك كل المقارنات:
تخبر = default المترجمَ بأن يقارن الأعضاء بترتيب التصريح، وهو بالضبط الترتيب المعجمي الذي كنت ستكتبه يدويًا. فضّل هذا الأسلوب على المترجمات الحديثة.
معامل الإسناد ومزالقه
operator= (إسناد النسخ) خاص: يولّده المترجم لك، وبالنسبة للأصناف البسيطة يكون هذا الافتراضي صحيحًا. لا تحتاج إلى كتابة معاملك الخاص إلا عندما يدير صنفك موردًا - ذاكرة خام أو مقبض ملف - حيث يكون النسخ عضوًا بعضو خاطئًا. التوقيع المعياري يُرجع *this بالمرجع كي يمكن تسلسل عمليات الإسناد (a = b = c):
يقبع مأزقان في هذه الدالة القصيرة. الأول هو فحص الإسناد الذاتي if (this == &other): بدونه، سيؤدي a = a إلى delete[] data ثم القراءة من other.data الذي حُرِّر للتو - سلوك غير معرّف. الثاني هو أن الترتيب مهم - في النسخة المكتوبة يدويًا يجب ألا تحذف المخزن المؤقت القديم قبل أن تنسخ الجديد بأمان (غالبًا ما يخصّص التطبيق الحقيقي الذاكرة أولًا، أو يستخدم نمط copy-and-swap، بحيث يترك التخصيص الفاشل الكائن سليمًا).
مأزق أعم: لا تحمّل المعاملات بطرق مفاجئة. فـ operator+ الذي يعدّل سرًا معامله التشغيلي الأول، أو operator== الذي ليس متماثلًا، سيربك كل قارئ ويكسر شيفرة المكتبة القياسية التي تفترض المعاني المعتادة. حمّل المعاملات فقط عندما تكون العملية فعلًا «شبيهة بالجمع» أو «شبيهة بالمساواة» لنوعك.
التالي: محدّدات الوصول
لاحظ كيف أبقى كل مثال أعضاء بياناته private وأظهر السلوك عبر سطح عام صغير - بانيات ومعاملات وبضع دوال. هذا الحد بين ما هو مرئي للعالم الخارجي وما هو مخفي داخل الصنف تتحكم فيه محدّدات الوصول: public و private و protected. سننظر بعد ذلك في ما يسمح به كل منها بالضبط، ولماذا تكون البيانات private مع دوال عامة هي الخيار الافتراضي للتغليف الجيد، وكيف يتلاءم protected مع الوراثة.
الأسئلة الشائعة
ما هو تحميل المعاملات الزائد في C++؟
يتيح لك تحميل المعاملات الزائد تحديد ما تعنيه المعاملات المدمجة مثل + أو == أو << بالنسبة لأنواعك الخاصة. تكتب دالة باسم خاص - operator+ و operator== وما إلى ذلك - ويستدعيها المترجم كلما ظهر المعامل مع معاملات تشغيلية من صنفك. هكذا يقوم string + string بالدمج ويطبع cout << obj كائنًا مخصصًا.
هل ينبغي أن تكون المعاملات دوالًا أعضاء أم دوالًا غير أعضاء (friend) في C++؟
استخدم دالة عضو عندما يكون المعامل التشغيلي الأول هو صنفك الخاص ولا يحتاج إلى تحويلات (مثل += و [] و ()). استخدم دالة غير عضو (غالبًا friend) عندما يمكن أن يكون المعامل التشغيلي الأول نوعًا مدمجًا أو عندما تريد تحويلات متماثلة على الجهتين؛ وهذا ضروري لـ operator<< لأن المعامل التشغيلي الأول هو std::ostream وليس صنفك.
ما المعاملات في C++ التي لا يمكن تحميلها زائدًا؟
لا يمكنك تحميل :: (تحليل النطاق) و . (الوصول إلى الأعضاء) و .* (الوصول عبر مؤشر العضو) و ?: (الثلاثي) و sizeof. كما لا يمكنك ابتكار معاملات جديدة كليًا ولا تغيير عدد المعاملات التشغيلية لمعامل ما أو أسبقيته - فالمعامل + ثنائي دائمًا وبالأسبقية نفسها سواء جمع قيم int أو نوعك Vector2.