ما هي المُحَوِّلات (Functors) في لغة C++ ولماذا نحتاجها؟
\n\nتخيل أنك تكتب كودًا بلغة C++ وتحتاج إلى تمرير سلوك (دالة) إلى دالة أخرى، على سبيل المثال، للفرز أو تصفية البيانات. الحل التقليدي هو المؤشرات إلى الدوال. لكن لها عيوب: فهي لا تخزن حالة، ويصعب تضمينها (inline)، وتبدو قديمة بعض الشيء. هنا تظهر على المسرح المُحَوِّلات في لغة C++ (وتُعرف أيضًا باسم كائنات الدوال أو الكائنات الوظيفية في C++).
\n\nالمُحَوِّل هو مجرد كلاس أو هيكل (struct) عادي، تم فيه إعادة تعريف عامل استدعاء الدالة operator(). بفضل هذا، يمكن استخدام نسخة من هذا الكلاس كدالة عادية. يبدو الأمر معقدًا؟ في الحقيقة، هي واحدة من أكثر أدوات C++ أناقة. على موقع Stack Overflow، حصد سؤال "What are C++ functors and their uses?" آلاف المشاهدات — المطورون يريدون فهم هذا الموضوع لكتابة كود أكثر مرونة وأعلى أداءً. في هذا الدليل، سنشرح كل شيء بالتفصيل: من بناء الجملة الأساسي إلى أمثلة الاستخدام الواقعية مع STL.
بناء الجملة الأساسي: كيف تنشئ أول مُحَوِّل لك
\n\nإنشاء مُحَوِّل يتلخص في ثلاث خطوات:
\n- \n
- تعريف كلاس أو هيكل (struct). \n
- تنفيذ
operator()داخله بالتوقيع المطلوب (المعاملات ونوع القيمة المُرجعة). \n - إنشاء كائن من هذا الكلاس واستدعاؤه كدالة. \n
إليك أبسط مثال على مُحَوِّل C++، يقوم بضرب رقم في اثنين:
\n\n#include <iostream>\n\nclass Multiplier {\npublic:\n // إعادة تعريف عامل ()\n int operator()(int x) const {\n return x * 2;\n }\n};\n\nint main() {\n Multiplier mult; // إنشاء كائن-دالة\n std::cout << mult(5) << std::endl; // الإخراج: 10\n std::cout << mult(10) << std::endl; // الإخراج: 20\n return 0;\n}\n\nلاحظ: mult(5) يبدو كاستدعاء لدالة عادية، لكنه في الحقيقة استدعاء لـ mult.operator()(5). يقوم المترجم بعمل سكر نحوي (syntactic sugar)، ويصبح الكود نظيفًا وبديهيًا.
الميزة الرئيسية: المُحَوِّلات يمكنها تخزين حالة
\n\nعلى عكس الدوال العادية أو تعبيرات لامدا (lambda) (قبل C++11)، يمكن لكائنات الدوال في C++ أن تحتوي على حقول خاصة بها. هذا يفتح إمكانيات هائلة. على سبيل المثال، يمكنك إنشاء مُحَوِّل لا يقوم فقط بتنفيذ إجراء، بل يتذكر أيضًا عدد مرات استدعائه:
\n\n#include <iostream>\n\nclass Counter {\nprivate:\n int count = 0;\npublic:\n int operator()() {\n return ++count;\n }\n int getCount() const { return count; }\n};\n\nint main() {\n Counter counter;\n std::cout << counter() << std::endl; // 1\n std::cout << counter() << std::endl; // 2\n std::cout << counter() << std::endl; // 3\n // الحالة محفوظة بين الاستدعاءات!\n return 0;\n}\n\nهذا المثال على مُحَوِّل C++ مع حالة لا يمكن تحقيقه باستخدام دالة بسيطة دون استخدام متغيرات عامة (وهو أمر سيء). المُحَوِّلات تحل هذه المشكلة بأناقة وأمان.
\n\nالمُحَوِّلات وخوارزميات STL: مزيج مثالي
\n\nأقوى استخدام للمُحَوِّلات هو العمل مع الخوارزميات من مكتبة القوالب القياسية (STL). الخوارزميات مثل std::sort، std::find_if، std::for_each تتوقع استقبال كائن قابل للاستدعاء (callable). يمكنك تمرير مُحَوِّل إليها، وسيقوم بتنفيذ المنطق المطلوب.
مثال: الفرز باستخدام مُحَوِّل
\n\nلنفترض أن لديك متجهًا (vector) من الأرقام، وتريد فرزها تنازليًا. يمكنك كتابة مُحَوِّل مقارنة خاص بك:
\n\n#include <iostream>\n#include <vector>\n#include <algorithm>\n\nclass Descending {\npublic:\n bool operator()(int a, int b) const {\n return a > b; // إرجاع true إذا كان a أكبر من b (ترتيب تنازلي)\n }\n};\n\nint main() {\n std::vector<int> vec = {3, 1, 4, 1, 5, 9, 2, 6};\n \n // تمرير كائن من Descending إلى std::sort\n std::sort(vec.begin(), vec.end(), Descending());\n \n std::cout << "المصفوفة بعد الفرز التنازلي: ";\n for (int n : vec) {\n std::cout << n << " ";\n }\n std::cout << std::endl;\n // الإخراج: 9 6 5 4 3 2 1 1\n return 0;\n}\n\nبدون المُحَوِّل، كان عليك إما كتابة دالة عادية وتمرير مؤشر إليها (وهو أقل مرونة)، أو استخدام تعبير لامدا (lambda). المُحَوِّل يمنحك تحكمًا كاملاً وإمكانية إعادة استخدام الكود.