Menu
Coddy logo textTech
flag Ar iconالعربيةdown icon

تعبيرات 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)، أو المقارنات المخصصة، أو معالجات الأحداث - دون تحديد كائنات دوال منفصلة.

challenge icon

التحدي

سهل

لنقم ببناء نظام معالج أحداث (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: اقرأ مدخلين:
    1. رقم أساسي (عدد صحيح)
    2. مضاعف (عدد صحيح)

    قم بإنشاء EventDispatcher واستعرض تقنيات التقاط لامبدا المختلفة عن طريق إضافة ثلاث دوال مرجعية:

    1. لامبدا تلتقط الرقم الأساسي بالقيمة (by value) وتطبع: Base value: [base]
    2. لامبدا تلتقط المضاعف بالمرجع (by reference)، وتزيده بمقدار 1، ثم تطبع: Multiplier after increment: [multiplier]
    3. لامبدا قابلة للتغيير (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;
}
quiz iconاختبر نفسك

يتضمن هذا الدرس اختبارًا قصيرًا. ابدأ الدرس للإجابة عليه وتتبّع تقدمك.

جميع دروس Object Oriented Programming