مفهوم الـ Generics في السي بلاس بلاس | C++ Generics


مفهوم الـ Generics في C++

الـ Generics هو أسلوب في كتابة الكود يجعلك قادراً على بناء كود واحد يلائم أكثر من نوع بيانات, و عند الحاجة إلى استخدام هذا الكود يمكنك إستخدامه كما هو أو تحديد نوع البيانات التي تريده أن يعمل معها حسب حاجتك.

كل كلاس, إنترفيس أو دالة تجد الرمز < > موجود ضمن تعريفه, إعرف مباشرةً أنه مصمم للتعامل مع أكثر من نوع بيانات.


 فائدة الـ Generics  في C++

أنت لست مجبر على استخدام أسلوب الـ Generics, لكن تعلمه سيفيدك كثيراً في تطوير الكود الخاص فيك, و جعل حجمه أصغر و أسهل في حالة التعديل.
فمثلاً, يمكنك بناء دالة يمكنك أن تمرر لها مصفوفة من أي نوع كان فتقوم بطباعة قيم العناصر الموجودة فيها.


مصطلحات تقينة

  • Generic Function: تعني تعريف دالة تتعامل مع أكثر من نوع بيانات.

  • Generic Class: تعني تعريف كلاس يتعامل مع أكثر من نوع بيانات.

  • Bounded Type Parameters: تعني تعريف كلاس أو دالة يتعاملان مع نوع محدد بالإضافة إلى جميع الأنواع المشتقة منه, أي التي ترث منه.

الأحرف المستخدمة في الـ Generics في C++

ذكرنا في الجدول التالي الأحرف المتعارف عليها بين المبرمجين عند التعامل مع الـ Generics.

الأحرف المتعارف عليها في الـ Generics
T إختصار للكلمة Type, يقصد منه أي نوع كان.
نستخدمه في العادة عند بناء كود يتعامل مع أي نوع بيانات.
N إختصار للكلمة Number, يقصد منه أي نوع من الأنواع التي تستخدم لتخزين الأرقام.
نستخدمه في العادة عند بناء كود يتعامل مع الأرقام من أي نوع كانت مثل ( int - float - double إلخ.. ).
E إختصار للكلمة Element, يقصد منه عنصر ليس له نوع محدد.
نستخدمه في العادة عند بناء كود يتعامل مع عناصر المصفوفة من أي نوع كانت.
K إختصار للكلمة Key, يقصد منه مفتاح ليس له نوع محدد.
نستخدمه في العادة عند بناء كود يتعامل مع مفاتيح كائن يخزن البيانات بشكل key / value.
V إختصار للكلمة Value, يقصد منه قيمة ليس لها نوع محدد.
نستخدمه في العادة عند بناء كود يتعامل مع قيم كائن يخزن البيانات بشكل key / value.

ملاحظة: جميع الأحرف المذكورة في الجدول ليس لها أي تأثير على الكود و يمكنك وضع أي حرف أو كلمة مكانهم, لكننا ننصحك باعتمادهم.

كيفية تعريف نوع غير محدد في C++

أي حرف ستستخدمه في دالة أو كلاس للإشارة إلى أن نوعه غير محدد, يجب تعريفه قبل تعريف الدالة أو الكلاس مباشرةً لأنه يعتبر خاص بما تعرّفه بعده فقط.


الشكل العام لتعريف نوع غير محدد هو كالتالي مع الإشارة إلى أنك تستطيع وضع أي حرف أو إسم تريد بدل الحرف T.

template <typename T>
	

في حال كنت تريد تعريف أكثر من نوع غير محدد, يجب أن تضع فاصلة بينهما كالتالي.

template <typename T1, typename T2>
	


في المثال التالي قمنا بتعريف دالة تقوم بطباعة قيمة أي متغير نمرره لها مهما كان نوعه.
بعدها قمنا باستدعائها ثلاث مرات مع تمرير قيمة مختلفة لها في كل مرة.

مثال
main.cpp
#include <iostream>
using namespace std;

// و هو خاص بالدالة التي سنقوم بتعريفها بعده E هنا قمنا بتعريف نوع بيانات غير محدد قمنا بتسميته
template <typename E>

// فتقوم بطباعتها x عند استدعائها نمرر لها قيمة من أي نوع كانت مكان الباراميتر printVar هنا قمنا بتعريف دالة إسمها
void printVar(E x)
{
    cout << x << endl;
}

// main() هنا قمنا بتعريف الدالة
int main()
{
	// string و متغير نوعه float متغير نوعه ,int هنا قمنا بتعريف متغير نوعه
	int x = 10;
	float y = 7.5;
	string name = "Mhamad";
	
	// ثلاث مرات مع تمرير قيمة لها نوع مختلف في كل مرة printVar() هنا قمنا باستدعاء الدالة
	printVar(x);
	printVar(y);
	printVar(name);
	
    return 0;
}
		

سنحصل على النتيجة التالية عند التشغيل.

10
75
Mhamad
		

النوع الذي تقوم بتعريفه قبل أي شيء يمكنك جعله ملاصق لها عند تعريفه و لكننا لم نفعل ذلك حتى نستطيع كتابة شرح واضح لك.

إذاً الدالة printVar() كان بإمكاننا تعريفها كالتالي و الأفضل أن نعرّفها كالتالي أيضاً.

template <typename E>
void printVar(E x)
{
    cout << x << endl;
}
	


عند استدعاء الدالة printVar() يمكنك تحديد نوع البيانات - التي تريد وضعها بنفسك مكان أي نوع غير محدد فيها - بدل جعل المترجم يفعل ذلك و هذا الأمر من شأنه جعل برنامجك أسرع في التنفيذ.

إذاً كان من المستحسن تحديد نوع القيمة التي سنمررلها للدالة printVar() عند استدعائها كالتالي.

printVar<int>(x);           // int      بشكل مباشر بالنوع printVar() في الدالة E هنا سيقوم المترجم بتديل الحرف
printVar<float>(y);         // float   بشكل مباشر بالنوع printVar() في الدالة E هنا سيقوم المترجم بتديل الحرف
printVar<string>(name);     // string بشكل مباشر بالنوع printVar() في الدالة E هنا سيقوم المترجم بتديل الحرف
	

أمثلة شاملة حول التعامل مع الـ Generics في C++


كيف تبني دالة يمكنك أن تمرر لها باراميتر ليس له نوع محدد  في C++

المثال التالي هو تطبيق لفكرة Generic Function حيث ستتعلم منه كيف تبني دالة يمكنك أن تمرر لها باراميتر ليس له نوع محدد.

في المثال التالي قمنا بتعريف الحرف E كنوع بيانات غير محدد.
بعدها قمنا بتعريف دالة إسمها printArray() عند استدعائها نمرر لها مصفوفة من أي نوع كان مع تمرير عدد عناصرها, فتقوم بطباعة جميع القيم الموجودة فيها.

مثال تطبيقي لفكرة Generic Function في C++

main.cpp
      #include <iostream>
      using namespace std;

      // و هو خاص بالدالة التي سنقوم بتعريفها بعده E هنا قمنا بتعريف نوع بيانات غير محدد قمنا بتسميته
      template <typename E>

      // length و حجم المصفوفة مكان الباراميتر arr عند استدعاءها نمرر لها مصفوفة من أي نوع مكان الباراميتر printArray هنا قمنا بتعريف دالة إسمها
      void printArray(E arr[], int length)
      {
      // على سطر واحد arr هنا قمنا بإنشاء حلقة تعرض جميع عناصر المصفوفة
      for(int i=0; i<length; i++)
      {
      cout << arr[i] << " ";
      }

      // بعد عرض جميع العناصر سيتم النزول على سطر جديد
      cout << endl;
      }

      // main() هنا قمنا بتعريف الدالة
      int main()
      {
      // هنا قمنا بتعريف ثلاث مصفوفات كل واحدة منهم تحتوي على نوع مختلف من البيانات
      int arr1[] = {1, 2, 3, 4, 5};
      char arr2[] = {'a', 'b', 'c', 'd', 'e'};
      string arr3[] = {"I'm", "learning" ,"C++", "in", "harmash.com"};

      // مع تحديد نوع كل مصفوفة سيتم تمريرها arr3 و arr2 و arr1 لطباعة قيم المصفوفات الثلاثة printArray() هنا قمنا باستدعاء الدالة
      printArray<int>(arr1, 5);
      printArray<char>(arr2, 5);
      printArray<string>(arr3, 5);

      return 0;
      }
    

سنحصل على النتيجة التالية عند التشغيل.

      1 2 3 4 5
      a b c d e
      I'm learning C++ in harmash.com
    

إذاً هنا في كل مرة تقوم فيها باستدعاء الدالة printArray() يتم تبديل الحرف E بشكل تلقائي بنوع المصفوفة التي تم تمريرها لها.



كيف تبني دالة ترجع قيمة ليس لها نوع محدد في C++

المثال التالي هو تطبيق لفكرة Generic Function أيضاً و لكنك ستتعلم منه كيف تبني دالة ترجع قيمة ليس لها نوع محدد.

في المثال التالي قمنا بتعريف الحرف T كنوع بيانات غير محدد.
بعدها قمنا بتعريف دالة إسمها divide() عند استدعائها نمرر لها عددين, فترجع ناتج قسمة العددين على بعضهما بالنوع الذي نريده.

الفكرة هنا أننا نريد جعل الدالة تقبل أعداد صحيحة و أعداد عشرية (فيها فاصلة) مع إمكانية الحصول على الناتج متضمناً أي أرقام تكون بعض الفاصلة أو بدونها.

مثال كيف تبني دالة ترجع قيمة ليس لها نوع محدد في C++

main.cpp
      #include <iostream>
      using namespace std;

      // و هو خاص بالدالة التي سنقوم بتعريفها بعده T هنا قمنا بتعريف نوع بيانات غير محدد قمنا بتسميته
      template <typename T>

      // فتقوم بإرجاع ناتج قسمتهما بالنوع الذي نريده y و x عند استدعائها نمرر لها عددين مكان الباراميترين divide هنا قمنا بتعريف دالة إسمها
      T divide(double x, double y)
      {
      return x/y;
      }

      // main() هنا قمنا بتعريف الدالة
      int main()
      {
      // int في الدالة الأصلية سيتم تبديله بالنوع T لمعرفة ناتج قسمة 5 على 2 مع تحديد أن الحرف divide() هنا قمنا باستدعاء الدالة
      cout << "divide<int>(5,2) = " << divide<int>(5,2) << endl;

      // double في الدالة الأصلية سيتم تبديله بالنوع T لمعرفة ناتج قسمة 5 على 2 مع تحديد أن الحرف divide() هنا قمنا باستدعاء الدالة
      cout << "divide<double>(5,2) = " << divide<double>(5,2);

      return 0;
      }
    

سنحصل على النتيجة التالية عند التشغيل.

      divide<int>(5,2) = 2
      divide<double>(5,2) = 2.5
    


كيفية بناء كلاس يتعامل مع مختلف الأنواع  في C++

المثال التالي هو تطبيق لفكرة Generic Class حيث ستتعلم منه كيفية بناء كلاس يتعامل مع مختلف الأنواع.

في المثال التالي قمنا بتعريف كلاس إسمه Box و يملك نوع بيانات مجهول رمزنا له بالحرف T.
في هذا الكلاس قمنا بتعريف متغير إسمه x نوعه T.
ثم قمنا بتعريف دالة إسمها getX() تستخدم لجلب قيمة المتغير x, و دالة إسمها setX() تستخدم لتحديد قيمة المتغير x.

في الأخير قمنا بتجربة إنشاء كائنين من الكلاس Box على النحو التالي:

  • الكائن الأول حددنا فيه أن قيمة x ستكون من النوع int.

  • الكائن الثاني حددنا فيه أن قيمة x ستكون من النوع string.

  • مثال كيفية بناء كلاس يتعامل مع مختلف الأنواع  في C++

main.cpp
 
      #include <iostream>
      using namespace std;

      // و هو خاص بالكلاس الذي سنقوم بتعريفه بعده T هنا قمنا بتعريف نوع بيانات غير محدد قمنا بتسميته
      // موضوع في الكلاس T سيتم تبديله بكل حرف Box عند إنشاء كائن من الكلاس T النوع الذي نحدده للحرف
      template <typename T>
      class Box
      {
      private:
      T x;

      public:
      void set(T x)
      {
      this->x = x;
      }

      T get()
      {
      return x;
      }
      };


      // main() هنا قمنا بتعريف الدالة
      int main()
      {
      // int في الكلاس الأصلي سيتم تبديله بالنوع T مع تحديد أن الحرف intBox إسمه Box هنا قمنا بتعريف كائن من الكلاس
      Box<int> intBox;

      // أيضاً int و الذي أصبح نوعه x سيتم تخزين هذه القيمة في المتغير .int هنا قمنا بإدخال قيمة فيه نوعها
      intBox.set(100);

      // أيضاً int القيمة التي سترجع هنا يكون نوعها .x هنا قمنا بإرجاع القيمة التي تم تخزينها في المتغير
      cout << "intBox contains: " << intBox.get() << endl;


      // string في الكلاس الأصلي سيتم تبديله بالنوع T مع تحديد أن الحرف stringBox إسمه Box هنا قمنا بتعريف كائن من الكلاس
      Box<string> stringBox;

      // أيضاً string و الذي أصبح نوعه x سيتم تخزين هذه القيمة في المتغير .string هنا قمنا بإدخال قيمة فيه نوعها
      stringBox.set("I can store string value");

      // أيضاً string القيمة التي سترجع هنا يكون نوعها .x هنا قمنا بإرجاع القيمة التي تم تخزينها في المتغير
      cout << "stringBox contains: " << stringBox.get();

      return 0;
      }
    

سنحصل على النتيجة التالية عند التشغيل.

      intBox contains: 100
      stringBox contains: I can store string value
    

معلومة تقنية

نقوم بتعريف Generic Class عندما يكون عندنا عمليات في هذا الكلاس يمكن أن تطبق على أكثر من نوع.
عندها بدل إنشاء كلاس للتعامل مع كل نوع على حدا, نقوم بتعريف كلاس عام Generic Class و نحدد النوع الذي نريد التعامل معه أثناء إنشاء كائن من هذا الكلاس.


تنبيه

عند إنشاء كائن من Generic Class تكون مجبر على تحديد كل أنواع البيانات الغير محددة فيه أو سيظهر لك خطأ Missing template arguments.