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

C++Lambda Expressions

مفهوم الـ Lambda Expressions في C++

إبتداءاً من الإصدر c++11 تم إضافة أسلوب جديد يمكن استخدامه لتقليل حجم الكود عند تعريف دوال جديدة.
الأسلوب الجديد الذي سنتعلمه في هذا الدرس لتعريف الدوال يقال له Lambda Expressions أو Closures أو Literals Function أو Lambdas فقط.


الشكل العامل لتعريف Lambda Expression في C++

[captures] (parameters) -> returnTypesDeclaration { lambdaStatements; };
	

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

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

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

مكان الكلمة lambdaStatements نضع الأوامر التي نريدها أن تتنفذ عند استدعاء الدالة.

أساسيات التعامل مع Lambda Expressions في C++

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

الدالة التي يتم تعريفها بأسلوب Lambda Expressions لا نعطيها إسم و لكننا نقوم بإسنادها لمتغير من نفس نوعها حتى نستدعيها من خلاله أون نجعل نوع المتغير الذي سيشير لها auto حتى يتولى المترجم وضع النوع المناسب لهذا المتغير.


في المثال التالي قمنا بتعريف دالة لا تملك أي باراميتر بأسلوب Lambda Expressions.
عند تعريفها قمنا بإسنادها لمتغير إسمه lambda و نوعه auto حتى نتمكن لاحقاً من استدعائها من خلاله. في الأخير قمنا باستدعاء الدالة بواسطة المتغير lambda و لاحظ كيف أننا وضعنا قوسين بعد إسم المتغير كما نستدعي الدالة العادية بالضبط.

مثال
Main.cpp
#include <iostream>

using namespace std;

int main()
{
	// يساويها حتى نستطيع لاحقاً استدعاءها من خلاله lambda و من ثم قمنا بتعريف متغير إسمه Lambda Expression هنا قمنا بتعريف دالة بأسلوب
	auto lambda = [] {
		cout << "My first lambda expression";
	};
	
	// و لاحظ أننا وضعنا قوسين بعده و كأننا نستدعي دالة عادية lambda هنا قمنا باستدعاء الدالة التي يشير لها المتغير
	lambda();
	
    return 0;
}
		

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

My first lambda expression
		

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

auto lambda = [] {
	cout << "My first lambda expression";
};
	

الطريقة الأولى: كان بإمكانك إضافة أقواس الباراميترات و تركها فارغة كالتالي.

auto lambda = []() {
	cout << "My first lambda expression";
};
	

الطريقة الثانية: كان بإمكانك وضع الكلمة void بين القوسين مما يعني أنه لا يوجد بارامتيرات أيضاً كالتالي.

auto lambda = [](void) {
	cout << "My first lambda expression";
};
	

الطريقة الثالثة: كان بإمكانك تحديد أن الدالة لا ترجع قيمة بعد أن يتم تنفيذها, أي ذكر أن نوعها void و هنا لا بد من وضع أقواس الباراميترات قبلها كالتالي.

auto lambda = []() void {
	cout << "My first lambda expression";
};
	

الطريقة الرابعة: كان بإمكانك وضع أقواس الباراميترات و بداخلها الكلمة void بالإضافة لذكر أن الدالة نوعها void أيضاً كالتالي.

auto lambda = [](void) void {
	cout << "My first lambda expression";
};
	

أمثلة شاملة حول طرق تعريف Lambda Expressions في C++


المثال الأول

في المثال التالي ستتعلم كيفية وضع بارميترات لدالة معرّفة بأسلوب Lambda Expressions بالإضافة إلى استدعائها.

في المثال التالي قمنا بتعريف دالة بأسلوب Lambda Expressions مع وضع باراميترين فيهما نوعهما int.
عند تعريفها قمنا بإسنادها لمتغير إسمه lambda و نوعه auto حتى نتمكن لاحقاً من استدعائها من خلاله.
مهمة هذه الدالة هي جمع العددين اللذين نمررهما لها و من ثم طباعة الناتج.
في الأخير قمنا باستدعاء الدالة بواسطة المتغير lambda و لاحظ كيف أننا وضعنا قوسين بعد إسم المتغير كما نستدعي الدالة العادية بالضبط.

مثال
Main.cpp
      #include <iostream>

      using namespace std;

      int main()
      {
      // و مهمتها طباعة ناتج العددين اللذين نمررهما void و نوعها int تحتوي على باراميترين نوعهما Lambda Expression هنا قمنا بتعريف
      // يساوي الدالة التي قمنا بتعريفها حتى نستطيع لاحقاً استدعاءها من خلاله lambda عند استدعاءها, كما أننا قمنا بتعريف متغير إسمه 
      auto lambda = [](int a, int b) void {
      cout << a << " + " << b << " = " << a + b;
      };

      // b و a مع تمرير العديد 3 و 5 لها مكان الباراميترين lambda هنا قمنا باستدعاء الدالة التي يشير لها المتغير
      lambda(3, 5);

      return 0;
      }
    

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

      3 + 5 = 8
    


المثال الثاني

في المثال التالي ستتعلم كيفية تعريف دالة بأسلوب Lambda Expressions ترجع قيمة بالإضافة إلى استدعائها.

في المثال التالي قمنا بتعريف دالة بأسلوب Lambda Expressions مع وضع باراميترين فيهما نوعهما int.
عند تعريفها قمنا بإسنادها لمتغير إسمه lambda و نوعه auto حتى نتمكن لاحقاً من استدعائها من خلاله.
مهمة هذه الدالة هي جمع العددين اللذين نمررهما لها و من ثم إرجاع الناتج.
في الأخير قمنا باستدعاء الدالة بواسطة المتغير lambda و لاحظ كيف أننا وضعنا قوسين بعد إسم المتغير كما نستدعي الدالة العادية بالضبط.

مثال
Main.cpp
      #include <iostream>

      using namespace std;

      int main()
      {
      // أيضاً int مع تحديد أنها ترجع قيمة نوعها int تحتوي على باراميترين نوعهما Lambda Expression هنا قمنا بتعريف
      // يساوي الدالة التي قمنا بتعريفها حتى نستطيع لاحقاً استدعاءها من خلاله lambda كما أننا قمنا بتعريف متغير إسمه 
      auto lambda = [](int a, int b) int {
      return a + b;
      };

      // مع تمرير العديد 3 و 5 لها lambda هنا قمنا باستدعاء الدالة التي يشير لها المتغير
      // و طباعة ناتج الجمع الذي سترجعه الدالة سيتم عرضه كما هو b و a مكان الباراميترين 
      cout << "3 + 5 = " << lambda(3, 5);

      return 0;
      }
    

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

      3 + 5 = 8
    


المثال الثالث

في المثال التالي ستتعلم كيفية تعريف دالة بأسلوب Lambda Expressions مع تمرير متغيرات خارجية لها بالإضافة إلى استدعائها.

في المثال التالي قمنا بتعريف متغيرين إسمهما a و b مع إعطائهما قيم أولية.
بعدها قمنا بتعريف دالة بأسلوب Lambda Expressions مع تمرير المتغيرين a و b لها كما هما.
عند تعريفها قمنا بإسنادها لمتغير إسمه lambda و نوعه auto حتى نتمكن لاحقاً من استدعائها من خلاله.
مهمة هذه الدالة هي جمع العددين اللذين نمررهما لها و من ثم إرجاع الناتج.

مثال
Main.cpp
      #include <iostream>

      using namespace std;

      int main()
      {
      int a = 3;
      int b = 5;

      // int لها, و تحديد أنها ترجع قيمة نوعها b و a مع تمرير المتغيرين Lambda Expression هنا قمنا بتعريف
      // يساوي الدالة التي قمنا بتعريفها حتى نستطيع لاحقاً استدعاءها من خلاله lambda كما أننا قمنا بتعريف متغير إسمه 
      auto lambda = [a, b]() int {
      return a + b;
      };

      // و من ثم قمنا بطباعة الناتج b و a حتى ترجع ناتج جمع العددين الموجودين في المتغيرين lambda هنا قمنا باستدعاء الدالة التي يشير لها المتغير
      cout << a << " + " << b << " = " << lambda();

      return 0;
      }
    

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

      3 + 5 = 8
    

معلومة تقنية

تمرير المتغير كما هو لدالة معرّفة بأسلوب Lambda Expressions يجعلك قادر على قراءة قيمها فقط بدون القدرة على تعديلها.
في حال أردت تحديث قيم المتغيرات التي يتم تمريرها للدالة فيجب تمرير عناوين المتغيرات لها و ليس أسماءها فقط.



المثال الرابع

في المثال التالي ستتعلم كيفية تعريف دالة بأسلوب Lambda Expressions مع تمرير عنواين متغيرات خارجية لها بالإضافة إلى استدعائها.

في المثال التالي قمنا بتعريف متغيرين إسمهما a و b مع إعطائهما قيم أولية.
بعدها قمنا بتعريف دالة بأسلوب Lambda Expressions مع تمرير عناوين المتغيرين a و b لها.
عند تعريفها قمنا بإسنادها لمتغير إسمه lambda و نوعه auto حتى نتمكن لاحقاً من استدعائها من خلاله.
مهمة هذه الدالة هي جمع العددين اللذين نمررهما لها و من ثم إرجاع الناتج.

مثال
Main.cpp
      #include <iostream>

      using namespace std;

      int main()
      {
      int a = 3;
      int b = 5;

      // int لها, و تحديد أنها ترجع قيمة نوعها b و a مع تمرير عناوين المتغيرين Lambda Expression هنا قمنا بتعريف
      // يساوي الدالة التي قمنا بتعريفها حتى نستطيع لاحقاً استدعاءها من خلاله lambda كما أننا قمنا بتعريف متغير إسمه 
      auto lambda = [&a, &b]() int {
      return a + b;
      };

      // و من ثم قمنا بطباعة الناتج b و a حتى ترجع ناتج جمع العددين الموجودين في المتغيرين lambda هنا قمنا باستدعاء الدالة التي يشير لها المتغير
      cout << a << " + " << b << " = " << lambda();

      return 0;
      }
    

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

      3 + 5 = 8
    


المثال الخامس

المثال التالي مهم جداً لأنه يعتبر الهدف الأساسي من ابتكار أسلوب Lambda Expressions و هي عادةً ما تستخدم لهذا الهدف بالتحديد, حيث أنك ستتعلم منه كيفية تمرير الدالة كباراميتر لدالة أخرى لحظة تعريفها.

تعريف الدوال بأسلوب Lambda Expressions عادةً ما يستخدم عند استخدام دالة تحتوي على باراميتر عبارة عن دالة أخرى.
في درس الحاويات الديناميكية ( STL ) و بالتحديد حين شرحنا كيفية التعامل مع الكلاس forward_list قمنا باستخدام دالة إسمها remove_if() و قلنا أن هذه الدالة يجب أن نمرر لها دالة أخرى عند استدعائها حتى نحدد كيف سيتم حذف العناصر من الكائن.

ملاحظة: يجب أن تراجع ذلك الدرس جيداً مع قراءة جميع الأمثلة الموضوعة فيه حتى تفهم ما سيتم شرحه الآن.


في ذاك الدرس و بالتحديد في المثال الخامس, قمنا بتعريف الدالة shouldBeRemoved() و من ثم تمريرها للدالة remove_if() كالتالي.

    // shouldBeRemoved() هكذا قمنا بتعريف الدالة
    bool shouldBeRemoved(const int& value)
    {
    if (value > 3)
    {
    return true;
    }

    return false;
    }

    ..
    ..

    // remove_if() و هكذا قمنا بتمريرها للدالة
    myList.remove_if(shouldBeRemoved);
  


إذاً ما فعلناه وقتها هو أننا قمنا بتعريف دالة عادية إسمها shouldBeRemoved() و مررناها للدالة Lambda Expressions و لكننا الآن سنقوم بإعادة نفس الكود السابقة و تعريف نفس الدالة السابقة بأسلوب Lambda Expressions مع تمريرها بشكل مباشر للدالة remove_if() كالتالي.

    myList.remove_if( [](const int& value) {
    if (value > 3)
    {
    return true;
    }

    return false;
    });
  


الآن إذا كنا سنعيد نفس المثال الخامس مع التركيز فقط على تمرير Lambda Expressions للدالة remove_if() فسيكون الكود كالتالي.

مثال
Main.cpp
      #include <iostream>
      #include <forward_list>

      using namespace std;

      int main()
      {
      // بالإضافة إلى أننا قمنا بإضافة عدة قيم فيه int يمكنه أن يحتوي على عناصر نوعها forward_list هنا قمنا بتعريف كائن من الكلاس
      forward_list<int> myList = {1, 3, 2, 4, 2, 5, 1, 5};

      // remove_if() لحذف جميع العناصر التي تستوفي الشرط الذي مررناه بين أقواس الدالة myList من الكائن remove_if() هنا قمنا باستدعاء الدالة
      // Lambda Expression لاحظ أننا قمنا بتمرير نفس الدالة التي قمنا بتمريرها في المثال في السابق و لكن بأسلوب remove_if() بين أقواس الدالة 
      myList.remove_if( [](const int& value) {
      // لإعلام المترجم أنه يجب حذف العنصر من الكائن الذي قام باستدعائها true هنا قلنا أن أي عنصر تكون قيمته أكبر من 3 سترجع الدالة
      if (value > 3)
      {
      return true;
      }

      // لإعلام المترجم أنه يجب عدم حذف العنصر من الكائن الذي قام باستدعائها false في حال لم تكن قيمة العنصر أكبر من 3 سيتم إرجاع القيمة
      return false;
      });

      cout << "List values = ";

      // إبتداءاً من أول عنصر وصولاً لآخر عنصر فيه myList هنا قمنا بإنشاء حلقة تقوم في كل دورة بطباعة قيمة عنصر جديد من العناصر الموجودة في الكائن
      for (auto it = myList.begin(); it != myList.end(); ++it)
      {
      cout << *it << " ";
      }

      return 0;
      }
    

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

      List values = 1 3 2 2 1