تعبيرات لامدا في C++: متى تعمل حقًا على تسريع الكود؟
\n\nمرحبًا، زميلي! إذا كنت قد ألقيت نظرة على Stack Overflow من قبل، فمن المحتمل أنك رأيت سؤالًا جمع آلاف المشاهدات: "ما هو تعبير لامدا، ومتى يجب أن أستخدم واحدًا؟". وهذا ليس من قبيل الصدفة. لامدا في C++ هي موضوع يبسط الحياة وفي نفس الوقت يثير الكثير من الجدل حول التحسين والأداء.
\n\nاليوم لن نقدم مجرد تعريف جاف. سنفهم كيف تعمل تعبيرات لامدا تحت الغطاء، ومتى يجعل استخدامها الكود أسرع، ومتى - على العكس - يخلق مشاكل خفية. هيا بنا!
\n\nما هو تعبير لامدا؟ نظرة من الداخل
\n\nرسميًا، تعبير لامدا هو دالة مجهولة يمكن تعريفها مباشرة في مكان الاستدعاء. ولكن إذا تعمقنا أكثر، يحول المترجم كل لامدا إلى كائن وظيفي فريد (functor). إليك مثال:
\n\n// لامدا\nauto add = [](int a, int b) { return a + b; };\n\n// ما يتحول إليه (مبسط)\nclass __lambda_1 {\npublic:\n auto operator()(int a, int b) const { return a + b; }\n};\n__lambda_1 add;\n\nالنقطة الأساسية: كل لامدا لها نوع فريد خاص بها. هذا يعني أن المترجم يمكنه تضمين الاستدعاءات بقوة - على عكس المؤشرات للدوال أو std::function.
الأداء: أين تتفوق لامدا؟
\n\nلنذهب مباشرة إلى صلب الموضوع. إليك ثلاثة سيناريوهات حيث تعطي لامدا زيادة ملحوظة في الأداء:
\n\n1. التضمين وغياب الاستدعاءات غير المباشرة
\n\nعندما تمرر لامدا إلى دالة قالب (مثل std::sort)، يعرف المترجم النوع الدقيق. هذا يسمح له بتضمين كود لامدا مباشرة في الدالة المستدعية. قارن:
// الخيار 1: مؤشر لدالة (أبطأ)\nbool compare(int a, int b) { return a < b; }\nstd::sort(vec.begin(), vec.end(), compare);\n\n// الخيار 2: لامدا (أسرع)\nstd::sort(vec.begin(), vec.end(), [](int a, int b) { return a < b; });\n\nفي الحالة الأولى - استدعاء غير مباشر عبر مؤشر. في الثانية - تضمين مباشر. على كميات كبيرة من البيانات (ملايين العناصر) يمكن أن يصل الفرق إلى 20-30% في الوقت.
\n\n2. التقاط المتغيرات بدون نسخ غير ضروري
\n\nتسمح لامدا بالتحكم المرن في الالتقاط. الاختيار الصحيح بين [=] و [&] والمتغيرات المحددة يؤثر مباشرة على الأداء:
std::vector<std::string> data;\nstd::string prefix = "prefix_";\n\n// سيء: نسخ سلسلة ثقيلة\nstd::for_each(data.begin(), data.end(), \n [=](std::string& s) { s = prefix + s; });\n\n// جيد: التقاط بالمرجع\nstd::for_each(data.begin(), data.end(), \n [&](std::string& s) { s = prefix + s; });\n\nالالتقاط بالقيمة ([=]) ينشئ نسخة من prefix داخل الكائن الوظيفي. بالنسبة لـ std::string قد يكون هذا مكلفًا. الالتقاط بالمرجع ([&]) خفيف الوزن.
3. لامدا في خوارزميات STL
\n\nتستخدم خوارزميات STL الحديثة (C++17/20) لامدا بنشاط لـ الحسابات المتوازية. على سبيل المثال، std::transform مع سياسة التنفيذ std::par:
std::vector<int> input(1000000);\nstd::vector<int> output(input.size());\n\n// معالجة متوازية\nstd::transform(std::execution::par, \n input.begin(), input.end(), output.begin(),\n [](int x) { return x * x + 2 * x + 1; });\n\nلامدا هنا هي المرشح المثالي للتوازي، حيث لا تحتوي على آثار جانبية (دالة نقية). يمكن للمترجم والمكتبة تشغيلها بأمان على عدة خيوط.
\n\nمتى يمكن أن تضر لامدا بالأداء؟
\n\nسيكون من غير العدل الحديث فقط عن الإيجابيات. هناك حالات حيث لامدا ليست الخيار الأفضل:
\n\n1. الالتقاط الثقيل بالقيمة
\n\nإذا قمت بالتقاط