تعبيرات Lambda بعمق
جزء من قسم Object Oriented Programming في رحلة C++ على Coddy — الدرس 85 من 104.
تُعد تعبيرات لامدا (Lambda expressions)، التي تم تقديمها في C++11، دوالاً مجهولة يمكنك تعريفها بشكل مضمن (inline). وبينما رأيت تعبيرات لامدا الأساسية مع خوارزميات STL، فإن فهم صيغتها الكاملة يفتح إمكانيات قوية لالتقاط المتغيرات والتحكم في كيفية الوصول إليها.
صيغة lambda الكاملة هي: [capture](parameters) mutable -> return_type { body }. تحدد capture clause (جملة الالتقاط) المتغيرات الخارجية التي يمكن لـ lambda الوصول إليها وكيفية ذلك:
#include <iostream>
int main() {
int x = 10;
int y = 20;
auto byValue = [x]() { return x * 2; }; // نسخة من x
auto byRef = [&y]() { y += 5; }; // مرجع لـ y
auto allByValue = [=]() { return x + y; }; // نسخ الكل
auto allByRef = [&]() { x++; y++; }; // مرجع للكل
auto mixed = [x, &y]() { y += x; }; // مزيج من الاثنين
byRef();
std::cout << y << "\n"; // 25
}بشكل افتراضي، تكون المتغيرات التي يتم التقاطها بالقيمة (captured-by-value) ثابتة const داخل تعبير لامبدا (lambda). تسمح الكلمة المفتاحية mutable بتعديل هذه النسخ:
int counter = 0;
auto increment = [counter]() mutable {
return ++counter; // تعديل نسخة اللامبدا
};
std::cout << increment() << "\n"; // 1
std::cout << increment() << "\n"; // 2
std::cout << counter << "\n"; // 0 - الأصل لم يتغيرأضافت C++14 عمليات التقاط التهيئة (init captures)، مما يتيح لك إنشاء متغيرات جديدة أو نقل الكائنات إلى تعبير لامبدا (lambda):
auto ptr = std::make_unique<int>(42);
auto lambda = [p = std::move(ptr)]() {
return *p;
}; // تم نقل الملكية إلى لامبداتعتبر تعبيرات لامدا (Lambdas) مفيدة بشكل خاص في البرمجة كائنية التوجه (OOP) عندما تحتاج إلى تمرير سلوك كمعامل - للاستدعاءات الراجعة (callbacks)، أو المقارنات المخصصة، أو معالجات الأحداث - دون تحديد كائنات دوال منفصلة.
التحدي
سهللنقم ببناء نظام معالج أحداث (event handler) يستعرض قوة تعبيرات لامبدا (lambda expressions) مع أوضاع التقاط (capture modes) مختلفة. ستقوم بإنشاء مرسل أحداث (event dispatcher) بسيط يقوم بتخزين واستدعاء الدوال المرجعية (callbacks)، مما يوضح كيف يمكن لـ lambdas التقاط الحالة الخارجية بطرق متنوعة.
ستقوم بتنظيم الكود الخاص بك عبر ثلاثة ملفات:
EventDispatcher.h: قم بتعريف فئةEventDispatcherالتي تدير الدوال المرجعية للأحداث.يجب أن يقوم المرسل الخاص بك بتخزين الدوال المرجعية باستخدام
std::vectorمن نوعstd::function<void()>. قم بتضمين هذه الأساليب (methods):addCallback(std::function<void()> callback)— يضيف دالة مرجعية إلى القائمة.fireAll()— يستدعي جميع الدوال المرجعية المخزنة بالترتيب.clear()— يزيل جميع الدوال المرجعية.
ستحتاج إلى تضمين
<functional>و<vector>.EventDispatcher.cpp: قم بتنفيذ الأساليب الخاصة بالمرسل. يجب أن يقوم الأسلوبfireAll()ببساطة بالتكرار عبر جميع الدوال المرجعية واستدعاء كل واحدة منها.main.cpp: اقرأ مدخلين:- رقم أساسي (عدد صحيح)
- مضاعف (عدد صحيح)
قم بإنشاء
EventDispatcherواستعرض تقنيات التقاط لامبدا المختلفة عن طريق إضافة ثلاث دوال مرجعية:- لامبدا تلتقط الرقم الأساسي بالقيمة (by value) وتطبع:
Base value: [base] - لامبدا تلتقط المضاعف بالمرجع (by reference)، وتزيده بمقدار 1، ثم تطبع:
Multiplier after increment: [multiplier] - لامبدا قابلة للتغيير (mutable) تلتقط متغير عداد (تمت تهيئته إلى 0) بالقيمة، وتزيده في كل مرة يتم استدعاؤها، وتطبع:
Call count: [counter]
بعد إضافة جميع الدوال المرجعية، قم باستدعاء
fireAll()مرتين لترى كيف تتصرف أوضاع التقاط المختلفة عبر استدعاءات متعددة. بين استدعاءيfireAll()، اطبع---كفاصل.أخيرًا، بعد كلتا الجولتين، اطبع القيمة النهائية لمتغير المضاعف من الدالة main لتوضيح كيف أثر التقاط المرجع عليه:
Final multiplier: [multiplier]
على سبيل المثال، مع المدخلات 10 و 5:
Base value: 10
Multiplier after increment: 6
Call count: 1
---
Base value: 10
Multiplier after increment: 7
Call count: 2
Final multiplier: 7مع المدخلات 42 و 0:
Base value: 42
Multiplier after increment: 1
Call count: 1
---
Base value: 42
Multiplier after increment: 2
Call count: 2
Final multiplier: 2لاحظ السلوكيات الرئيسية: التقاط القيمة يحافظ على الأساس الأصلي دون تغيير، والتقاط المرجع يعدل متغير المضاعف الفعلي في main (يتراكم عبر الاستدعاءات)، وعداد لامبدا القابلة للتغيير (mutable lambda) يستمر في الزيادة لأن كل fireAll() يستدعي نفس كائن لامبدا المخزن الذي يحتفظ بنسخته الداخلية الخاصة.
ورقة مرجعية
تعبيرات Lambda هي دوال مجهولة (anonymous functions) يمكن تعريفها بشكل مضمن (inline). الصيغة الكاملة هي:
[capture](parameters) mutable -> return_type { body }جمل الالتقاط (Capture Clauses)
تحدد جملة الالتقاط المتغيرات الخارجية التي يمكن لـ lambda الوصول إليها وكيفية ذلك:
int x = 10;
int y = 20;
auto byValue = [x]() { return x * 2; }; // نسخة من x
auto byRef = [&y]() { y += 5; }; // مرجع لـ y
auto allByValue = [=]() { return x + y; }; // نسخ جميع المتغيرات
auto allByRef = [&]() { x++; y++; }; // الإشارة إلى جميع المتغيرات كمرجع
auto mixed = [x, &y]() { y += x; }; // مزيج من كلا الوضعينتعبيرات Lambda القابلة للتغيير (Mutable Lambdas)
بشكل افتراضي، تكون المتغيرات التي يتم التقاطها بالقيمة (captured-by-value) ثابتة const داخل lambda. استخدم mutable لتعديل النسخ:
int counter = 0;
auto increment = [counter]() mutable {
return ++counter; // يعدل نسخة lambda
};
std::cout << increment() << "\n"; // 1
std::cout << increment() << "\n"; // 2
std::cout << counter << "\n"; // 0 - الأصل لم يتغيرالتقاطات التهيئة (Init Captures) (C++14)
إنشاء متغيرات جديدة أو نقل الكائنات إلى lambda:
auto ptr = std::make_unique<int>(42);
auto lambda = [p = std::move(ptr)]() {
return *p;
}; // تم نقل الملكية إلى lambdaاستخدام Lambdas مع std::function
تخزين lambdas في std::function لمعالجات الأحداث (event handlers) والاستدعاءات الراجعة (callbacks):
#include <functional>
#include <vector>
std::vector<std::function<void()>> callbacks;
callbacks.push_back([x]() { /* استخدام x */ });
callbacks.push_back([&y]() { /* تعديل y */ });جرّب بنفسك
#include <iostream>
#include "EventDispatcher.h"
using namespace std;
int main() {
int base;
int multiplier;
cin >> base;
cin >> multiplier;
EventDispatcher dispatcher;
// TODO: أضف lambda تلتقط base بالقيمة (BY VALUE)
// يجب أن تطبع: "Base value: [base]"
// TODO: أضف lambda تلتقط multiplier بالمرجع (BY REFERENCE)
// يجب أن تزيد multiplier بمقدار 1، ثم تطبع: "Multiplier after increment: [multiplier]"
// TODO: أضف lambda من نوع MUTABLE تلتقط عداداً (تمت تهيئته بـ 0) بالقيمة
// يجب أن تزيد العداد وتطبع: "Call count: [counter]"
// TODO: استدعِ fireAll() لتنفيذ جميع دوال الاستدعاء (callbacks)
// TODO: اطبع "---" كفاصل
// TODO: استدعِ fireAll() مرة أخرى
// TODO: اطبع قيمة multiplier النهائية: "Final multiplier: [multiplier]"
return 0;
}
يتضمن هذا الدرس اختبارًا قصيرًا. ابدأ الدرس للإجابة عليه وتتبّع تقدمك.
جميع دروس Object Oriented Programming
1أساسيات الـ OOP
الملفات الخارجيةبناء وتجميع (Compilation) لغة C++ملفات الـ Header وملفات الـ Sourceالـ Namespaces والـ Scopeمقدمة في الـ OOP في C++الـ Classes مقابل الـ Objectsالمؤشر 'this'الـ Methods (الدوال الأعضاء)الـ Attributes (بيانات الأعضاء)أساسيات الـ Ctors والـ Dtorsمراجعة - آلة حاسبة بسيطة4خصائص الـ Class
أعضاء الـ Instance مقابل الـ Staticدوال الـ Getters والـ Settersدوال الأعضاء الثابتة (Const)الكلمة المفتاحية Mutableالدوال والمتغيرات الساكنة (Static)الدوال والفئات الصديقة (Friend)مراجعة - مدير الحساب البنكي7الوراثة
الوراثة الأساسيةمستويات الوصول في الوراثةترتيب استدعاء الـ Ctor والـ Dtorإعادة تعريف الدوال (Method Overriding)الدوال الافتراضية والـ VTableالوراثة المتعددةالوراثة الافتراضيةمراجعة - هيكلية الموظفين10نظرة عامة على STL
نظرة عامة وفلسفة STLحاويات STLالـ Iteratorsخوارزميات STLالـ Functors وتعبيرات Lambdaمراجعة - تكرار الكلمات13أنماط التصميم - الجزء الأول
مقدمة في أنماط التصميمنمط Singletonنمط Factory و Abstract Factoryنمط Builderنمط Observerنمط Strategy2إدارة الذاكرة
ذاكرة Stack مقابل Heapالمؤشرات والمراجعالذاكرة الديناميكية (new/delete)المؤشرات الذكية في C++RAII في C++مراجعة - مدير المصفوفات الديناميكية5التغليف (Encapsulation)
محددات الوصول في C++محددات الوصول بشكل متعمقإخفاء المعلوماتStruct مقابل Classالأصناف المتداخلة والداخليةمراجعة - نظام سجلات الطلاب8تعدد الأشكال (Polymorphism)
تعدد الأشكال: وقت التجميع مقابل وقت التشغيلالتحميل الزائد للدوال (Function Overloading)مراجعة الدوال الافتراضية (Virtual Functions)الدوال الافتراضية البحتة (Pure Virtual Functions)الأصناف المجردة (Abstract Classes)تصميم الواجهات (Interface) في C++التحويل الديناميكي (Dynamic Casting) و RTTIملخص - حاسبة الأشكال11مفاهيم OOP المتقدمة
التركيب مقابل الوراثةالـ Mixins عبر CRTPنمط Pimplمحو النوع (Type Erasure)أصناف Enum والأنواع القويةمعالجة الاستثناءات في OOPتسلسلات الاستثناءات المخصصة14أنماط التصميم - الجزء الثاني
نمط الأمرنمط المحولنمط المزيننمط قالب الطريقةنمط الحالةنمط التركيبRAII كنمط3المنشئات والموادم
المنشئ الافتراضيالمنشئ ذو المعاملاتمنشئ النسخمنشئ النقلقوائم تهيئة المنشئالمنشئات المفوضةتعمق في الموادمقاعدة الثلاثة / الخمسة / الصفرمراجعة - فئة String6تحميل العوامل (Operator Overloading)
مقدمة في تحميل العواملتحميل العوامل الحسابيةتحميل عوامل المقارنةعوامل Streamتحميل عامل التعيينتحميل العوامل [] و ()عوامل تحويل النوعمراجعة - Matrix Class9القوالب
قوالب الدوالقوالب الأصنافتخصيص القوالبالقوالب المتغيرةأساسيات SFINAE و Type Traitsمراجعة - الحاويات العامة12ميزات C++ الحديثة
دلالات النقل و Rvaluesالتمرير المثاليتعبيرات Lambda بعمقstd::function و std::bindconstexpr و constevalالارتباطات المهيكلةoptional و variant و any