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

قوالب C++‎: شرح الدوال والأصناف العامة

اكتب الكود مرة واحدة ودعه يعمل مع كل نوع باستخدام قوالب C++‎ - قوالب الدوال، وقوالب الأصناف، واستنتاج النوع، ورسائل أخطاء المُصرّف المُربكة التي تتسبب فيها.

تحتوي هذه الصفحة على محررات قابلة للتشغيل - حرّر، شغّل، وشاهد النتيجة فوراً.

اكتبه مرة واحدة، واستخدمه مع كل نوع

في الصفحة السابقة رتّبت vector<int> باستخدام std::sort. لكن std::sort يرتّب أيضاً vector<string> وvector<double> ومصفوفة من بُناك الخاصة - من دون أن يكتب أحد دالة sort منفصلة لكل منها. هذا ليس سحراً وليس تحميلاً زائداً (overloading). إنه قالب: قطعة كود واحدة يُعيد المُصرّف استخدامها لأي نوع تسلّمه إياه.

من دون القوالب، ستكون عالقاً في نسخ المنطق نفسه ولصقه لكل نوع. إليك الدالة maximum نفسها مكتوبة ثلاث مرات، وهذا بالضبط التكرار الذي وُجدت القوالب لتقضي عليه:

int    maximum(int a, int b)       { return a > b ? a : b; }
double maximum(double a, double b) { return a > b ? a : b; }
string maximum(string a, string b) { return a > b ? a : b; }

الأجسام متطابقة. الأنواع فقط هي التي تختلف. يتيح لك القالب أن تقول "هذا يعمل مع أي نوع T" وأن تكتبه مرة واحدة.

قوالب الدوال

تحوّل دالة إلى قالب بإضافة template <typename T> أمامها واستخدام T في كل موضع يأتي فيه عادةً نوع محدد.

لاحظ أنك لم تكتب أبداً maximum<int> ولا maximum<double>. ينظر المُصرّف إلى الوسائط ويستنتج ما الذي ينبغي أن يكون عليه T - هذا هو استنتاج وسائط القالب. كل نوع مختلف تستدعيها به يدفع المُصرّف إلى تنصيب (توليد) دالة محددة منفصلة خلف الكواليس.

يمكنك ذكر النوع صراحةً عندما يعجز الاستنتاج، باستخدام الأقواس الزاوية:

يكمن في الاستنتاج فخ شائع. بما أن T يجب أن يكون نوعاً واحداً، فإن خلط أنواع الوسائط يُفسده:

maximum(3, 7.5);   // خطأ: هل T من النوع int أم double؟ يرفض المُصرّف التخمين.

يمكنك إصلاح ذلك بالتصريح - maximum<double>(3, 7.5) - أو بإعطاء كل معامل معامل نوعٍ خاصاً به، وهو ما سنفعله تالياً.

معاملات الأنواع المتعددة

القالب ليس محصوراً بنوع واحد. اذكر منها ما تحتاجه، مفصولة بفواصل. هكذا تكتب دالة يمكن أن تكون معاملاتها من أنواع مختلفة:

عندما يعتمد نوع القيمة المُعادة على المعاملات، دع المُصرّف يحلّه باستخدام auto (في C++14 وما بعده)، الذي يتناغم بطبيعته مع القوالب:

قوالب الأصناف

القوالب ليست للدوال فحسب - يمكن جعل الأصناف كاملةً قوالب. هكذا تعمل الحاويات القياسية تماماً: فـ vector<int> وmap<string, int> وpair<A, B> كلها قوالب أصناف. تكتب بنية البيانات مرة واحدة فتخزّن أي نوع تُمرّره كمعامل.

إليك Box عاماً صغيراً يحمل قيمة واحدة من أي نوع:

الفرق الجوهري عن قوالب الدوال: مع قالب الصنف يتعين عليك عادةً تزويد النوع بين أقواس زاوية - Box<int> - لأنه في المعايير الأقدم لا توجد وسائط بانٍ يُستنتج منها النوع. (أضاف C++17 استنتاج وسائط قالب الصنف، فصار Box b(42); يعمل أيضاً، لكن التصريح آمن دائماً ويُقرأ بوضوح.)

ستكون الأخطاء هائلة - إليك السبب

هذا هو الجزء الذي يتعثر فيه الجميع، لذا يستحق أن يُقال بصراحة. لا يُفحص القالب فحصاً كاملاً إلا عند تنصيبه بنوع حقيقي. يمكنك كتابة قالب يستخدم <، ويُصرَّف بسلاسة بمفرده - ولا يظهر الخطأ إلا في اللحظة التي تُنصّبه فيها بنوع لا يملك <.

template <typename T>
T maximum(T a, T b) {
    return a > b ? a : b;   // يتطلب أن يدعم T العامل >
}

struct Point { int x, y; };

// maximum(Point{1,2}, Point{3,4});
// خطأ: لا يوجد operator > للنوع Point. تذكر الرسالة Point وتقتبس
// كذلك هذه الدالة بأكملها، ممتدةً غالباً عبر أسطر كثيرة.

لأن المُصرّف يُحلّ النوع الكامل داخل القالب ويُبلّغ عن الإخفاقات من داخل الكود المُولَّد، يمكن لخطأ واحد أن يُنتج جداراً من المُخرجات يذكر تفاصيل المكتبة الداخلية. نصيحتان للنجاة:

  • اقرأ الخطأ الأول، لا الأخير. فالأخطاء اللاحقة عادةً نتيجة تبعية للأول.
  • تفحّص الرسالة بحثاً عن اسم النوع الخاص بك (هنا Point). فهذا يخبرك أي تنصيب أخفق.

الحل الحقيقي هو التأكد من أن نوعك يدعم كل ما يحتاجه القالب - وبالنسبة إلى maximum، يعني ذلك منح Point العامل operator>، وهو موضوع صفحة لاحقة. يمكن لـ المفاهيم (concepts) في C++20 الحديث أن تُقدّم هذه الأخطاء إلى مرحلة أبكر وتجعلها مقروءة، لكن نموذج الإحلال الكامن تحتها هو نفسه.

التالي: الأصناف

لقد بنيت للتوّ قالب الصنف Box - صنفاً ببيانات خاصة، وبانٍ، ودوال عضو - بينما كان تركيزك على القوالب. تتمهّل الصفحة التالية وتُعلّم الأصناف كما يجب: كيف تجمع البيانات مع الدوال التي تعمل عليها، وما الذي يتحكم فيه public وprivate فعلاً، وكيف تصل دوال العضو إلى حالة الكائن نفسه. تتضافر القوالب والأصناف باستمرار في C++ الواقعية، لذا فإن الإلمام المتين بالأصناف يجعل كتابة الكود العام أسهل بكثير.

الأسئلة الشائعة

ما هو القالب في C++‎؟

القالب هو مخطط يتيح لك كتابة دالة أو صنف مرة واحدة، ثم يولّد المُصرّف نسخة لكل نوع تستخدمه معه. تكتب template <typename T> ثم تستخدم T بديلاً عن النوع الحقيقي. يُنتج المُصرّف نسخة محددة - وهذا ما يُسمى التنصيب (instantiation).

ما الفرق بين typename وclass في قالب C++‎؟

في قائمة معاملات القالب، تعني template <typename T> وtemplate <class T> الشيء نفسه تماماً. يُفضَّل عموماً اليوم استخدام typename لأنه يُقرأ بصدق أكبر: فـ T يمكن أن يكون أي نوع، وليس صنفاً فقط. اختيار الكلمة المفتاحية لا يؤثر إطلاقاً على الكود المُولَّد.

لماذا تكون رسائل أخطاء قوالب C++‎ طويلة إلى هذا الحد؟

تُفحص القوالب عند تنصيبها بنوع حقيقي، لا عند كتابتها. إذا كان النوع لا يدعم عملية استخدمتها (مثل < للترتيب)، يظهر الخطأ في أعماق كود المكتبة مع النوع المُنصَّب كاملاً مكتوباً بالتفصيل، مُنتجاً صفحات من المُخرجات. اقرأ الخطأ الأول وابحث فيه عن اسم النوع الخاص بك.

Coddy programming languages illustration

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

ابدأ الآن