اسم واحد، نسخ كثيرة
كثيرًا ما تحتاج إلى نفس العملية لأنواع مختلفة من البيانات: طباعة int، وطباعة string، وطباعة double. في بعض اللغات قد تخترع printInt وprintString وprintDouble. أمّا C++ فتتيح لك إعطاءها جميعًا نفس الاسم، وتميّز بينها بمعاملاتها. هذا هو التحميل الزائد للدوال.
القاعدة بسيطة: يمكن لعدة دوال أن تتشارك اسمًا واحدًا طالما اختلفت قوائم معاملاتها؛ في عدد المعاملات أو أنواعها أو ترتيبها. ينظر المُترجِم إلى الوسائط عند كل موضع استدعاء ويختار لك النسخة المطابِقة.
ثلاث دوال، اسم واحد. يصل كل استدعاء إلى النسخة التي يلائم نوع معاملها الوسيطَ. هذا هو ما يجعل std::cout << x يعمل مع الأعداد الصحيحة والأعداد العشرية والسلاسل النصية على حدّ سواء؛ فـoperator<< محمّل زائدًا مرّاتٍ عديدة.
ما الذي يُعدّ نسخةً مختلفة
تتميّز النسخة بقائمة معاملاتها فقط. يمكنك أن تغيّر:
int area(int side); // معامل واحد
int area(int width, int height); // معاملان -> مختلفة
double area(double r); // نوع مختلف -> مختلفة
void log(string msg, int level); // الترتيب مهم...
void log(int level, string msg); // ...لذا هذه أيضًا مختلفة
كلٌّ منها نسخة مشروعة ومستقلة. يبني المُترجِم مجموعة مرشّحين من كل الدوال المسمّاة area، ثم يطابق حسب عدد الوسائط ونوعها.
نوع القيمة المعادة وحده لا يكفي
ها هي العقبة التي يتعثّر فيها الجميع تقريبًا: لا يمكنك التحميل الزائد اعتمادًا على نوع القيمة المعادة. لا تؤدّي القيمة المعادة أيّ دور في اختيار النسخة، لأن المُترجِم يقرّر أيّ دالة يستدعي من الوسائط قبل أن ينظر إطلاقًا إلى ما يُرجَع.
int convert(double x); // OK
double convert(double x); // خطأ: إعادة تعريف - نوع القيمة المعادة فقط هو ما يختلف
لن يُترجَم هذا. إذا كانت قوائم المعاملات متطابقة، فإن التصريحين هما نفس الدالة من حيث التحميل الزائد، وتحصل على خطأ إعادة تعريف. للتفريع بحسب نوع النتيجة، غيّر معاملًا (أو استخدم القوالب / تحويل النوع عبر static_cast عند موضع الاستدعاء بدلًا من ذلك).
كيف يختار حلّ التحميل الزائد فائزًا
عندما تُجري استدعاءً، يرتّب المُترجِم كل النسخ القابلة للاستخدام ويختار أفضل تطابق. على نحوٍ تقريبي، يفضّل بهذا الترتيب:
- تطابقًا تامًّا (لا حاجة إلى تحويل).
- ترقية (مثل
charأوshort->int، وfloat->double). - تحويلًا قياسيًا (مثل
int->double، وdouble->int، ومؤشّر إلى الصنف الأساس).
إذا كانت نسخة واحدة بالضبط أفضل بصرامة من كل ما عداها، فهي الفائزة. لاحظ كيف يتغلّب التطابق التام على التحويل:
'A' هو من نوع char، لكنّ الترقية إلى int تتفوّق على التحويل إلى double، لذا يُستدعى تحميل int. قواعد الترتيب هذه هي السبب في أن حلّ التحميل الزائد عادةً "يفعل الصواب تمامًا" — ويفاجئك من حينٍ لآخر.
فخّ الغموض
إذا كانت نسختان متساويتين في الجودة — ولا واحدة أفضل بصرامة — يرفض المُترجِم التخمين ويبلّغ عن استدعاء غامض. الحالة المدرسية هي نسختان تحتاج كلٌّ منهما إلى تحويلٍ من نفس الرتبة:
void f(int x);
void f(double x);
f(0L); // خطأ: غامض - long -> int و long -> double تحويلان من نفس الرتبة
لا int ولا double تطابق تام لـlong، وكلا التحويلين على نفس الرتبة، فيصبح الاستدعاء غامضًا. لديك حلّان نظيفان:
مفاجأة ذات صلة: تمرير سلسلة نصية حرفية. ستتنافس كلٌّ من void g(const string&) وvoid g(bool) على g("hi")، وقد تفوز bool، لأن const char* يتحوّل إلى bool (غير الصفري -> true) بخطواتٍ أقلّ من بناء std::string. إن رأيت يومًا سلسلةً حرفية تستدعي على نحوٍ غامض نسخة bool لديك، فهذا هو السبب — أضِف نسخة const char* أو const string& لتأخذ التطابق التام.
التحميل الزائد والوسائط الافتراضية لا ينسجمان جيدًا
الوسائط الافتراضية ليست بديلًا عن التحميل الزائد، والجمع بينهما يخلق غموضًا. إذ يمكن لكلٍّ منهما أن يجيب على نفس الاستدعاء، فلا يستطيع المُترجِم الاختيار:
void connect(string host, int port = 8080); // يمكن استدعاؤها بوسيط واحد
void connect(string host); // يمكن استدعاؤها أيضًا بوسيط واحد
connect("localhost"); // خطأ: غامض - كلتاهما تطابق وسيطًا واحدًا
اختر نهجًا واحدًا لكل شكل استدعاء. استخدم الوسائط الافتراضية عندما يكون السلوك متطابقًا وتريد فقط معاملات اختيارية؛ واستخدم التحميل الزائد عندما يجب أن تشغّل قوائمُ الوسائط المختلفة شِفرةً مختلفة فعلًا. أمّا مزجهما بحيث يتصادم توقيعان لنفس عدد الوسائط فهو خطأ غموض مضمون.
تمييز أخير يستحق الترسيخ: التحميل الزائد ليس التجاوز (override). يُحَلّ التحميل الزائد وقت الترجمة بين دوالٍ في نفس النطاق بنفس الاسم لكن بمعاملات مختلفة. أمّا التجاوز فيستبدل دالة virtual في صنفٍ مشتقّ وقت التشغيل ويتطلّب نفس التوقيع — وهو موضوع نتناوله في الدوال الافتراضية لاحقًا.
التالي: تعابير لامبدا
يمنح التحميل الزائد اسمًا واحدًا عدة تنفيذات مُحدَّدة الأنواع تُختار وقت الترجمة. لكنّك أحيانًا لا تريد دالة مسمّاة على الإطلاق — تحتاج إلى دالة صغيرة عابرة تُعرَّف في موضع استخدامها تمامًا، غالبًا لتمريرها إلى خوارزمية مثل sort. هذا بالضبط ما هي عليه تعابير لامبدا: دوال مجهولة يمكنك كتابتها ضمن السطر، والتقاط المتغيّرات المحيطة بها، وتمريرها في تعبير واحد. سنرى تاليًا كيف نكتبها ومتى تتفوّق على دالة مسمّاة كاملة.
الأسئلة الشائعة
ما هو التحميل الزائد للدوال في C++؟
يتيح لك التحميل الزائد للدوال تعريف عدة دوال بـنفس الاسم طالما اختلفت قوائم معاملاتها (في العدد أو النوع أو الترتيب). يختار المُترجِم أيها يُستدعى بناءً على الوسائط التي تمرّرها، لذا يمكن أن تستدعي print(42) وprint("hi") دالتي print مختلفتين.
هل يمكن أن تختلف دالتان في C++ بنوع القيمة المعادة فقط؟
لا. يجب أن تختلف الدوال المحمّلة زائدًا في قائمة معاملاتها. تسبّب int f(int) وdouble f(int) خطأ في الترجمة؛ فنوع القيمة المعادة ليس جزءًا من التوقيع المستخدَم في حلّ التحميل الزائد، لأن المُترجِم يختار النسخة من الوسائط عند موضع الاستدعاء، قبل أن تُستعمل القيمة المعادة أصلًا.
ما الذي يسبّب خطأ "الاستدعاء الغامض" مع الدوال المحمّلة زائدًا؟
يحدث عندما تكون نسختان متطابقتين بنفس الجودة ولا يستطيع المُترجِم تفضيل إحداهما. مثال كلاسيكي هو f(int) وf(double) مع الاستدعاء f(0L) (من نوع long)، حيث تتطلب كلتاهما تحويلًا من نفس الرتبة. أصلِح ذلك بإضافة نسخة بتطابق تام أو بتحويل الوسيط إلى النوع الذي تريده.