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

دوال لامدا في ++C: شرح الدوال المجهولة مع أمثلة

اكتب دوالًا صغيرة مضمَّنة في الحال باستخدام دوال لامدا في ++C - الصياغة، وكيف يعمل الالتقاط، ومتى تستخدم mutable، وفخّ الالتقاط المعلَّق الذي يقع فيه الجميع.

تحتوي هذه الصفحة على محررات قابلة للتشغيل - حرّر، شغّل، وشاهد النتيجة فوراً.

دوال تكتبها حيث تستخدمها

في الصفحة السابقة رأيت كيف يتيح التحميل الزائد لعدّة دوال أن تتشارك اسمًا واحدًا. لكن أحيانًا لا تريد دالة مسمّاة على الإطلاق - تحتاج إلى قطعة منطق صغيرة مرة واحدة، تمامًا حيث تستخدمها، وتسميتها لن تضيف سوى الفوضى. هذا هو ما تكون عليه دالة لامدا: دالة مجهولة يمكنك تعريفها مضمَّنة.

لدالة لامدا شكل مميَّز من أربعة أجزاء:

[capture](parameters) -> return_type { body }

العلامة [] هي الدليل على أنك تنظر إلى دالة لامدا. نوع الإرجاع اختياري - فالمترجم يستنتجه عادةً. وإليك أبسط صورة ممكنة:

greet هو مجرد متغير (نوعه غير قابل للنطق، لذا تخزّنه باستخدام auto) يمكنك استدعاؤه بـ (). دوال لامدا ذات المعاملات تعمل تمامًا مثل الدوال العادية:

الالتقاط: الوصول إلى النطاق المحيط

الجزء الذي يجعل دوال لامدا أكثر من مجرد دوال بلا اسم هو قائمة الالتقاط - أي []. فهي تتيح لدالة لامدا استخدام متغيرات من النطاق الذي عُرّفت فيه، لا معاملاتها الخاصة فحسب.

التقط بالقيمة عبر [x]: تحصل دالة لامدا على نسختها الخاصة، مجمَّدة في لحظة إنشاء دالة لامدا.

لاحظ أن scale(5) طبع 50، مستخدمًا قيمة factor التي كانت 10 وقت إنشاء دالة لامدا. الالتقاط بالقيمة يأخذ لقطة.

التقط بالمرجع عبر [&x]: تشير دالة لامدا إلى المتغير الأصلي، فترى التغييرات اللاحقة ويمكنها تعديله.

يمكنك أيضًا التقاط كل ما تستخدمه دالة لامدا عبر [=] (الكل بالقيمة) أو [&] (الكل بالمرجع). وهما مريحان، لكن الصراحة - [total] أو [&total] - توثّق بالضبط ما تمسّه دالة لامدا، وتجعل الاستدلال عليها أسهل.

فخّ المرجع المعلَّق

الالتقاط بالمرجع قوي وخطير بالقدر نفسه. المرجع صالح فقط ما دام المتغير الأصلي على قيد الحياة. فإن بقيت دالة لامدا أطول مما التقطته، حصلت على مرجع معلَّق وسلوك غير معرَّف - قد ينهار البرنامج، وقد يطبع قمامة، وقد يبدو أنه يعمل مصادفةً.

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

auto makeCounter() {
    int count = 0;
    return [&count]() { return ++count; };  // خطأ: count يموت هنا
}
// دالة لامدا المُرجَعة تشير الآن إلى ذاكرة قد دُمّرت.

عندما تُرجِع makeCounter، يُدمَّر متغيرها المحلي count، لكن دالة لامدا ما زالت تحمل مرجعًا إليه. استدعاء دالة لامدا المُرجَعة يمسّ ذاكرة ميتة. الإصلاح هو الالتقاط بالقيمة كي تمتلك دالة لامدا حالتها الخاصة:

قاعدة عامة: التقط بالمرجع فقط حين تُستخدم دالة لامدا فورًا ومحليًا (كما مع الخوارزميات أدناه). أما في اللحظة التي تُخزَّن فيها دالة لامدا أو تُرجَع أو تُشغَّل لاحقًا، ففضّل الالتقاط بالقيمة.

‏mutable وأنواع الإرجاع

هل لاحظت الكلمة mutable في المثال الأخير؟ افتراضيًا، يكون الالتقاط بالقيمة const داخل دالة لامدا - يمكنك قراءة النسخة لكن لا يمكنك تغييرها. وإضافة mutable تتيح لدالة لامدا أن تعدّل نسخها الخاصة بين الاستدعاءات.

mutable تؤثر فقط على النسخة الخاصة بدالة لامدا - أما seen الخارجي فيبقى دون مساس، وهذا هو جوهر الالتقاط بالقيمة بأكمله.

في معظم الأحيان يستنتج المترجم نوع الإرجاع على نحو جيد. ولا تحتاج إلى ذكره صراحةً عبر -> إلا عند وجود التباس، كأن تكون دالة لامدا قد تُرجِع أنواعًا مختلفة في فروع مختلفة:

// بدون -> لا يستطيع المترجم الاختيار بين int وdouble
auto half = [](int n) -> double {
    if (n % 2 == 0) return n / 2;   // int
    return n / 2.0;                 // double
};

دوال لامدا والخوارزميات: المكسب الحقيقي

السبب وراء إضافة دوال لامدا إلى ++C هو تغذية خوارزميات المكتبة القياسية بقطع منطق قصيرة. قبل دوال لامدا، كان عليك كتابة دالة مسمّاة منفصلة أو كائن دالة مرهَق بعيدًا عن المكان الذي يُستخدم فيه. أما الآن فالمنطق يقيم تمامًا عند موضع الاستدعاء.

أشهر مثال هو ترتيب فرز مخصّص:

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

ولأن دوال لامدا هذه تُستخدم فورًا ولا تبقى أطول من الدالة المحيطة، فإن الالتقاط بالمرجع ([&passMark]) سيكون آمنًا هنا أيضًا - لكن الالتقاط بالقيمة لا يقل وضوحًا، ولا يصبح معلَّقًا أبدًا.

التالي: المؤشرات

طرحت دوال لامدا بهدوء سؤالًا أعمق: حين تلتقط [&x]، تكون دالة لامدا متمسّكة بـ_موضع_ x، وذلك الموضع يبقى صالحًا فقط ما دام x حيًّا. تلك الفكرة - قيمة تشير إلى موضع وجود شيء في الذاكرة، وما يحدث حين يختفي ما تشير إليه - هي بالضبط موضوع الصفحة التالية. سنواجه المؤشرات وجهًا لوجه: كيف تأخذ عنوانًا، وكيف تتبعه، وكيف تظهر المشكلة ذاتها من التعليق التي رأيتها للتو في جميع أنحاء ++C.

الأسئلة الشائعة

ما هي دالة لامدا في ++C؟

دالة لامدا هي دالة مجهولة يمكنك كتابتها مضمَّنة، تمامًا في المكان الذي تستخدمها فيه. الصياغة هي [captures](parameters){ body }. وهي مثالية للعمليات القصيرة لمرة واحدة - مثل المقارن الذي تمرّره إلى std::sort - دون الحاجة إلى التصريح بدالة مسمّاة منفصلة في مكان آخر.

ما الفرق بين الالتقاط بالقيمة والالتقاط بالمرجع في دالة لامدا في ++C؟

تلتقط [x] نسخة من x مجمَّدة في لحظة إنشاء دالة لامدا. أما [&x] فتلتقط مرجعًا إلى x الأصلي، فترى الدالة التغييرات اللاحقة ويمكنها تعديله. استخدم [&] فقط طالما أنه مضمون أن المتغيرات الملتقَطة ستبقى أطول من دالة لامدا، وإلا حصلت على مرجع معلَّق.

لماذا تقول دالة لامدا في ++C إنها لا تستطيع تعديل متغير ملتقَط؟

تكون عمليات الالتقاط بالقيمة const افتراضيًا داخل دالة لامدا. أضف الكلمة المفتاحية mutable - [x]() mutable { x++; } - لتسمح لدالة لامدا بتغيير نسختها الخاصة. لاحظ أن هذا يغيّر نسخة دالة لامدا فقط، وليس المتغير الأصلي في الخارج.

Coddy programming languages illustration

تعلّم البرمجة مع Coddy

ابدأ الآن