اذهب إلى المحتوى

دوال لامدا Lambdas في Cpp


محمد بغات

ما هي تعابير لامدا؟

توفّر دوال لامدا طريقة موجزة لإنشاء كائنات دوالّ بسيطة، وتعبير لامدا هو قيمة يمينية خالصة (prvalue) تُنتجُ كائنَ تغليف (closure)، والذي يتصرف ككائن دالة.

نشأ الاسم "تعبير لامدا" (lambda expression) عن علم حسابيات اللامدا (lambda calculus)، وهو شكل رياضي اختُرِع في الثلاثينيات من قِبل Alonzo Church للإجابة عن بعض الأسئلة المتعلقة بالمنطق والحوسبيّات (computability)، وقد شكّلت حسابيات لامدا أساس لغة LISP، وهي لغة برمجة وظيفية.

مقارنة بحسابيات لامدا و LISP، فإنّّ تعبيرات لامدا في C++‎ تشترك معها في أنّها غير مسمّاة، وأنّها تلتقط المتغيّرات الموجودة في السياق المحيط، لكنّها تفتقر إلى القدرة على العمل على الدوال أو إعادتها.

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

يتألف تعبير لامدا عادة من ثلاثة أجزاء: قائمة الالتقاط ‎[]‎، وقائمة المعاملات الاختيارية ‎()‎، والمتن ‎{}‎، وكلها يمكن أن تكون فارغة:

[]() {}    // تعبير لامدا فارغ لا يعيد أي شيء.

قائمة الالتقاط (Capture list)

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

int a = 0;    // تعريف متغير عددي صحيح
auto f =[]()
{
    return a * 9;
};    // Error: 'a' cannot be accessed
auto f =[a]()
{
    return a * 9;
};    // بالقيمة  'a' تم التقاط
auto f =[& a]()
{
    return a++;
};    // بالمرجع  'a' تم التقاط
//     a ملاحظة: المبرمج ليس مسؤولا عن ضمان ألا تُدمَّر
//        قبل استدعاء لامدا

auto b = f();    // من دالة الالتقاط ولم تُمرّر هنا a استدعاء دالة لامدا، تم أخذ

قائمة المعاملات

تمثّل ‎()‎ قائمة المعاملات، وهي تماثل قائمة المعاملات في الدوال العادية، ويمكن حذف الأقواس إذا لم يأخذ تعبير لامدا أيّ وسائط، إلا إذا كنت بحاجة إلى جعل تعبير لامدا قابلًا للتغيير ‎mutable‎. يُعدُّ تعبيرا لامدا التاليان متكافئين:

auto call_foo =[x]()
{
    x.foo();
};
auto call_foo2 =[x]
{
    x.foo();
};

الإصدار ≥ C++‎ 14

تستطيع قائمةُ المعاملات أن تستخدم النوعَ النائب (placeholder type)‏ ‎auto‎ بدلاً من الأنواع الفعلية، وهذا يجعل الوسيط يتصرّف كمُعامل قالب لقالب دالة. تعبيرات لامدا التالية متكافئة:

auto sort_cpp11 =[](std::vector<T>::const_reference lhs, std::vector<T>::const_reference rhs)
{
    return lhs < rhs;
};
auto sort_cpp14 =[](const auto &lhs, const auto &rhs)
{
    return lhs < rhs;
};

متن الدالة

القوسان المعقُوصان ‎{}‎ يحتويان على متن تعبير لامدا، ويُماثل متن الدوال العادية.

استدعاء دالة لامدا

يعيد تعبير لامدا مغلِّفًا أو غلافًا (closure)، ويمكن استدعاؤه باستخدام الصيغة ‎operator()‎ (كما هو الحال مع الدوال العادية):

int multiplier = 5;
auto timesFive =[multiplier](int a)
{
    return a * multiplier;
};
std::out << timesFive(2);    // 10
multiplier = 15;
std::out << timesFive(2);    // 2 *5 == 10

نوع القيمة المعادة

افتراضيًا، يُستنبَط نوع القيمة المُعادة لتعبير لامدا تلقائيًا.

[]()
{
    return true;
};

في هذه الحالة، نوع القيمة المُعادة هو ‎bool‎، يمكنك أيضًا تعريف نوع القيمة المُعادة يدويًا باستخدام الصياغة التالية:

[]()->bool
{
    return true;
};

دوال لامدا القابلة للتغيير (Mutable Lambda)

الكائنات المُلتقطة بالقيمة في دوال لامدا تكون افتراضيًا غير قابلة للتغيير (immutable)، وذلك لأنّ المُعامل ‎operator()‎ الخاصّ بالكائن المغلّف المُنشأ ثابت (‎const‎) افتراضيًا. في المثال التالي، سيفشل التصريف لأن ++C ستحاول محاكاة حالة تعبير لامدا.

auto func = [c = 0](){++c; std::cout << c;};

يمكن السماح بالتعديل باستخدام الكلمة المفتاحية ‎mutable‎، والتي تجعل المُعامل ‎operator()‎ غير ثابت ‎const‎:

auto func =[c = 0]() mutable
{
    ++c;
    std::cout << c;
};

إذا اسُتخدِم مع نوع القيمة المُعادة، فينبغي وضع ‎mutable‎ قبله.

auto func =[c = 0]() mutable->int
{
    ++c;
    std::cout << c;
    return c;
};

انظر المثال التالي لتوضيح فائدة لامدا:

الإصدار < C++‎ 11

// كائن دالي عام لأجل المقارنة
struct islessthan
{
    islessthan(int threshold): _threshold(threshold) {}

    bool operator()(int value) const
    {
        return value < _threshold;
    }

    private:
        int _threshold;
};
// التصريح عن متجهة
const int arr[] = { 1, 2, 3, 4, 5 };
std::vector<int> vec(arr, arr + 5);
// إيجاد عدد أصغر من مُدخَل مُعطى - على افتراض أنه سيكون مُدخلا إلى دالة
int threshold = 10;
std::vector<int>::iterator it = std::find_if(vec.begin(), vec.end(), islessthan(threshold));

الإصدار ≥ C++‎ 11

// التصريح عن متجهة
std::vector<int> vec
{
    1, 2, 3, 4, 5
};
// إيجاد عدد أصغر من مُدخَل مُعطى - على افتراض أنه سيكون مُدخلا إلى دالة
int threshold = 10;
auto it = std::find_if(vec.begin(), vec.end(), [threshold](int value)
{
    return value < threshold;
    });

يمكننا تلخيص ما سبق بالجدول التالي:

المعامل التفاصيل
default-capture الالتقاط الافتراضي يحدّد كيفيّة التقاط جميع المتغيّرات غير المدرجة، قد يكون عبر ‎=‎ (التقاطًا بالقيمة) أو عبر ‎&‎ (التقاطا بالمرجع). وفي حال حذفها، سيتعذّر الوصول إلى المتغيّرات غير المدرجة ضمن متن لامدا. كما يجب أن يسبق default- capture المعاملُ capture-list.
capture-list لائحة الالتقاط يحدّد كيف يمكن الوصول إلى المتغيّرات المحلية داخل متن لامدا، تُلتقط المتغيّرات التي ليست لها سابقة (prefix) بالقيمة (by value) بينما تُلتقط المتغيّرات المسبوقة بـ ‎&‎ بالمرجع. يمكن استخدام ‎this‎ في توابع الأصناف لجعل جميع أعضاء الكائن الذي يشير إليه في المتناول عبر المرجع. ولا يمكن الوصول إلى المتغيّرات غير المدرجة في القائمة إلا إذا كانت القائمة مسبوقة بـ default-capture.
argument-list قائمة الوسائط يحدّد وسائط دالّة لامدا.
mutable (اختياري) عادة ما تكون المتغيّرات الملتقطة بالقيمة ثابتة (‎const‎). وستكون غير ثابتة عند تحديد ‎mutable‎. وسيُحتفظ بالتغييرات على تلك المتغيّرات بين الاستدعاءات.
throw-specification رفع الاعتراضات (اختياري) يحدّد سلوك رفع الاعتراضات في دالّة لامدا، مثل: ‎noexcept‎ أو ‎throw‎‏ (std::exception).
attributes السمات (اختياري) سمات دالّة لامدا. على سبيل المثال، إذا كان متن لامدا يلقي دائمًا اعتراضًا، فيمكن استخدام ‎[[noreturn]]‎.
نوع القيمة المعادة (اختياري) يحدّد نوع القيمة المُعادة من دالّة لامدا. وهي ضرورية في الحالات التي لا يكون بمقدور المٌصرّف فيها تخمين نوع القيمة المعادة.
lambda-body متن لامدا الكتلة التي تحتوي تنفيذ لامدا.

تحديد نوع القيمة المُعادة

بالنسبة لدوال لامدا التي تحتوي عبارة return واحدة فقط، أو عدّة عبارات return من نفس النوع، فيمكن للمصرّف أن يستنتج نوع القيمة المُعادة. انظر المثال التالي الذي يعيد قيمة بوليانية لأن "10 < value" هي قيمة بوليانية.

auto l =[](int value)
{
    return value > 10;
}

بالنسبة لدوال لامدا التي لها عدّة عبارات return من أنواع مختلفة، فلا يمكن للمصرّف أن يستنتج نوع القيمة المُعادة:

// error: return types must match if lambda has unspecified return type
auto l =[](int value)
{
    if (value < 10)
    {
        return 1;
    }
    else
    {
        return 1.5;
    }
};

في هذه الحالة، يجب عليك تحديد نوع القيمة المُعادة بشكل صريح:

// 'double' حُدِّد نوع القيمة المعادة بـ
auto l =[](int value)->double
{
    if (value < 10)
    {
        return 1;
    }
    else
    {
        return 1.5;
    }
};

تطابق هذه القواعدُ قواعدَ استنتاج نوع ‎auto‎، ولا تعيد دوال لامدا التي لم يُصرّح بنوع قيمتها المُعادة صراحة مراجعًا أبدًا، لذا إذا كنت تودّ إعادة نوع مرجعي (reference type)، فيجب تحديده بشكل صريح: تعيد copy في المثال التالي قيمة من نوع &X لذا تنسخ المدخلات:

auto copy = [](X& x) { return x; };   

لا يحدث نسخ لأن ref في المثال التالي ستعيد قيمة من نوع &X:

auto ref = [](X& x) -> X& { return x; };   

الالتقاط بالقيمة Capture by value

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

int a = 0;
[a]()
{
    return a;    // بالقيمة 'a' تم التقاط
};

الإصدار < C++‎ 14

auto p = std::unique_ptr<T>(...);
[p]()
{
    // Compile error; `unique_ptr` is not copy-constructible
    return p->createWidget();
};

من C++‎ 14 وصاعدًا، من الممكن تهيئة المتغيّرات على الفور، وهذا يسمح بالتقاط "أنواع النقل فقط" في لامدا.

الإصدار ≥ C++‎ 14

auto p = std::make_unique<T> (...);
[p = std::move(p)]()
{
    return p->createWidget();
};

رغم أنّ دوال لامدا يمكن أن تلتقط المتغيّرات بالقيمة عندما تُمرّر باسمائها، إلّا أنّ تلك المتغيّرات لا يمكن تعديلها في متن لامدا افتراضيًا، وذلك لأنّ نوع المغلّف يضع متن لامدا في تصريح من الشكل ‎operator() const‎.

وتُطبَّق ‎const‎ على محاولات الوصول إلى المتغيّرات العضوية (member variables) في نوع المغلّف، والمتغيّرات المُلتقطة التي هي أعضاء في المغلّف (على خلاف المتوقّع):

int a = 0;
[a]()
{
    a = 2;    // غير جائز
    decltype(a) a1 = 1;
    a1 = 2;    // جائز
};

لإزالة ‎const‎، يجب عليك وضع الكلمة المفتاحية ‎mutable‎ في تعبير لامدا:

int a = 0;
[a]() mutable
{
    a = 2;    // 'a' يمكن الآن تعديل
    return a;
};

نظرًا لأنّ ‎a‎ التُقِطت بالقيمة، فإنّّ أيّ تعديلات تُجرى عن طريق استدعاء تعبير لامدا لن تؤثر على ‎a‎، ولقد نُسخِت قيمة ‎a‎ في تعبير لامدا عند إنشائه، لذلك فإنّ نسخة ‎a‎ في تعبير لامدا مختلفة عن المتغيّر ‎a‎ الخارجي.

int a = 5 ;
auto plus5Val = [a] (void) { return a + 5 ; } ;
auto plus5Ref = [&a] (void) {return a + 5 ; } ;
a = 7 ;
std::cout << a << ", value " << plus5Val() << ", reference " << plus5Ref() ;
// "7, value 10, reference 12"

تعبيرات لامدا الذاتية الاستداعاء Recursive lambdas

لنفترض أنّنا نرغب في كتابة خوارزمية اقليدس للقاسم المشترك الأكبر ‎gcd()‎ على شكل تعبير لامدا. أولا، سنكتبها على شكل دالة:

int gcd(int a, int b) {
return b == 0 ? a : gcd(b, a%b);
}

تعبيرات لامدا لا يمكن أن تكون ذاتية الاستدعاء، لأنها غير قادرة على استدعاء نفسها، فلامدا ليس لها اسم، واستخدام ‎this‎ داخل متن لامدا إنّما يشير إلى ‎this‎ المُلتقَط (بافتراض أنّ تعبير لامدا مُنشأ في متن دالة تابعة، وإلا فسيُطلق خطأ)، إذن كيف نحلّ هذه المشكلة؟

استخدام std::function

يمكننا جعل تعبير لامدا يحصل على مرجع إلى كائن ‎std::function‎ لم يُنشأ بعد:

std::function<int(int, int)> gcd = [&](int a, int b){
return b == 0 ? a : gcd(b, a%b);
};

هذا سيعمل بدون مشاكل، ولكن ينبغي ألّا تستخدم هذه الطريقة إلّا لُمامًا، إذ أنّها بطيئة (لأنّنا نستخدم محو الأنواع - type erasure - بدلاً من استدعاء الدالّة المباشرة)، كما أنّها هشّة (نسخ ‎gcd‎ أو إعادة ‎gcd‎ سيكسر الشيفرة لأنّ تعبير لامدا يشير إلى الكائن الأصلي)، ولن تعمل مع دوال لامدا العامة.

استخدام مؤشرين ذكيين

auto gcd_self = std::make_shared<std::unique_ptr< std::function<int(int, int)> >>();
*gcd_self = std::make_unique<std::function<int(int, int)>>(
[gcd_self](int a, int b){
return b == 0 ? a : (**gcd_self)(b, a%b);
    };
};

هذا يضيف الكثير من الأعمال غير المباشرة (ما يؤدّي إلى زيادة الحِمل)، ولكن يمكن نسخه/إعادته، كما أنّ جميع النسخ تشترك في الحالة، وهذا يتيح لك إعادة تعبير لامدا، وهذا الحل أفضل من الحل المذكور أعلاه.

استخدام Y-combinator

باستخدام بِنية صغيرة مُساعِدة، يمكننا حل جميع هذه المشاكل:

template < class F>
    struct y_combinator
    {
        F f;    // لامدا ستُخزَّن هنا
        // مسبق operator() عامل
        template < class...Args >
            decltype(auto) operator()(Args && ...args) const
            {

ستمرر نفسها هنا إلى f ثم تمرر الوسائط، ويجب أن تأخذ لامدا الوسيط الأول كـ auto&& recurse أو نحوه، نتابع المثال:

             return f(*this, std::forward<Args> (args)...);
            }
    };
// :دالة مساعِدة تستنبط نوع لامدا
template < class F>
    y_combinator<std::decay_t < F>> make_y_combinator(F && f)
    {
        return {
            std::forward<F> (f)
        };
    }

// ( `make_` هناك حلول أفضل من C++17 تذكر أنّ في )

يمكننا تنفيذ ‎gcd‎ على النحو التالي:

auto gcd = make_y_combinator(
[](auto&& gcd, int a, int b){
return b == 0 ? a : gcd(b, a%b);
}
);

‎y_combinator‎ هو أحد مفاهيم حسابيّات لامدا، ويتيح لك العمل بالذاتية دون الحاجة إلى تسمية الدالة، وهذا هو بالضبط ما ينقصنا في دوال لامدا. تستطيع إنشاء تعبير لامدا يأخذ "recurse" كوسيط أوّل، وعندما تريد استخدام الذاتية، يمكنك تمرير الوسائط إلى recurse.

ويعيد ‎y_combinator‎ بعد ذلك كائن دالة يستدعي تلك الدالّة مع وسائطها، ولكن بكائن "recurse" المناسب (أي ‎y_combinator‎ نفسه) كوسيط أوّل، ويوجِّه بقية الوسائط التي مرّرتها إلى ‎y_combinator‎ إلى تعبير لامدا على النحو التالي:

auto foo = make_y_combinator( [&](auto&& recurse, some arguments) {
{
    // اكتب شيفرة تعالج بعض المعاملات
});

استدع recurse مع بعض الوسائط الأخرى عند الحاجة لتنفيذ الذاتية (Recursion)، وكذلك نحصل على الذاتية في لامدا دون أيّ قيود أو حِمل كبير.

الالتقاط الافتراضي Default capture

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

int a = 1;
int b = 2;
// التقاط افتراضي بالقيمة
[=]() { return a + b; };    // بالقيمة a و b تم التقاط
// الالتقاط الافتراضي بالمرجع
[&]() { return a + b; };    // بالمرجع a و b 

ما يزال بالإمكان القيام بالتقاط صريحٍ بجانب الالتقاط الافتراضي الضِّمني، سيعيد الالتقاط الصريح تعريف الالتقاط الافتراضي:

int a = 0;
int b = 1;
[=, &b]() 
{
    a = 2;   
    b = 2;   
};

في الشيفرة السابقة: لا تجوز a = 2 لأن a ملتقطَة بالقيمة، وتعبير لامدا لا يقبل التغيير، أي ليس mutable، بينما تجوز b = 2 لأن b ملتقَطة بالمرجع.

دوال لامدا في الأصناف والتقاط this

تُعدُّ تعبيرات لامدا الذي تم تقييمها في تابع ما صديقة للصّنف الذي ينتمي إليه ذلك التابع ضمنيًا:

class Foo
{
    private:
        int i;

    public:
        Foo(int val): i(val) {}

    // تعريف دالة تابعة
    void Test()
    {
        auto lamb = [](Foo &foo, int val)
        {
            // (private) تعديل متغير عضو خاص
            foo.i = val;
        };

يُسمح لـ lamb أن تصل إلى عضو خاص لأنها صديقة لـ Foo، نتابع المثال:

        lamb(*this, 30);
    }
};

مثل هذه التعبيرات ليست صديقة لهذا الصنف فحسب، بل لها نفس إمكانيات الوصول التي يتمتّع بها الصنف الذي صُرِّح عنها فيه. كذلك يمكن لدَوال لامدا التقاط المؤشّر ‎this‎، والذي يمثّل نُسخة الكائن الذي استُدعِيت الدالّة الخارجية عليه، عن طريق إضافة ‎this‎ إلى قائمة الالتقاط، انظر المثال التالي:

class Foo
{
    private:
        int i;

    public:
        Foo(int val): i(val) {}

    void Test()
    {
        // بالقيمة this التقاط المؤشر
        auto lamb =[this](int val)
        {
            i = val;
        };

        lamb(30);
    }
};

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

هذا يعني أيضًا أنّ لامدا يمكنها تعديل ‎this‎ حتى لو لم تكن قابلة للتغيير (‎mutable‎)، ذلك أنّ المؤشّر هو الذي يكون ثابتًا ‎const‎ وليس الكائن الذي يؤشّر إليه، إلا إن كان التابع الخارجي دالة ثابتة ‎const‎.

أيضًا، تذكّر أنّ كتل الالتقاط الافتراضية ‎[=]‎ و ‎[&]‎ ستلتقط ‎this‎ ضمنيًا، وكلاهما سيلتقطَانه بقيمة المؤشّر، وفي الحقيقة، فمن الخطأ تحديد ‎this‎ في قائمة الالتقاط عند إعطاء قيمة افتراضية.

الإصدار ≥ C++‎ 17

وتستطيع دوال لامدا التقاط نسخة من كائن ‎this‎ المُنشأ في وقت إنشاء تعبير لامدا، وذلك عن طريق إضافة ‎*this‎ إلى قائمة الالتقاط:

class Foo
{
    private:
        int i;

    public:
        Foo(int val): i(val) {}

    void Test()
    {
        // التقاط نسخة من الكائن المُعطى من قبل المؤشر
        auto lamb =[*this](int val) mutable
        {
            i = val;
        };

        lamb(30);    // this->i لا تغيّر
    }
};

الالتقاط بالمرجع Capture by reference

إذا وضعت السابقة ‎&‎ قبل اسم متغيّر محلي، فسيُلتقط المتغيّر بالمرجع، هذا يعني نظريًا أنّ نوع مُغلِّفِ لامدا سيكون له متغيّر مرجعي يُهيَّأُ كمرجع، وسيشير ذلك المرجع إلى المتغيّر المقابل الموجود خارج نطاق تعبير لامدا، وأيّ استخدام للمتغيّر في متن لامدا سوف يشير إلى المتغيّر الأصلي:

// 'a' التصريح عن المتغير
int a = 0;
// بالمرجع 'a' التصريح عن لامدا تلتقط
auto set =[& a]()
{
    a = 1;
};
set();
assert(a == 1);

الكلمة المفتاحية ‎mutable‎ ليست مطلوبة لأنّ ‎a‎ نفسها ليست ثابتة. والالتقاط بالمرجع يعني أنّ لامدا يجب ألا تخرج عن نطاق المتغيّرات التي تلتقطها، لذلك يمكنك استدعاء الدوال التي تأخذ دالّة، ولكن لا تستدع دالّة تخزّن لامدا خارج نطاق مراجعك، وكذلك لا تُعِد تعبير لامدا.

تعابير لامدا العامة النوع Generic lambdas

الإصدار ≥ C++‎ 14

يمكن أن تأخذ دوال لامدا وسائط من أيّ نوع، هذا يسمح لهذه الدوال أن تكون أكثر عمومية:

auto twice =[](auto x)
{
    return x + x;
};
int i = twice(2);    // i == 4
std::string s = twice("hello");    // s == "hellohello"

طُبِّق هذا في C++‎ عبر جعل ‎operator()‎ تزيد تحميل دالّة قالب (template function)، انظر النوع التالي الذي له سلوك مكافئ لسلوك مُغلِّف لامدا أعلاه:

struct _unique_lambda_type
{
    template < typename T>
        auto operator()(T x) const
        {
            return x + x;
        }
};

ليس بالضرورة أن تكون كل المعاملات عامة في تعبير لامدا غير المحدد أو العام (Generic lambda):

[](auto x, int y)
{
    return x + y;
}

هنا، تُستنبَط ‎x‎ بناءً على وسيط الدالّة الأوّل، بينما سيكون ‎y‎ دائمًا عددًا صحيحًا (‎int‎)، وقد تأخذ دوال لامدا العامَّة الوسائط بالمرجع أيضًا، وذلك باستخدام القواعد المعتادة لـ ‎auto‎ و ‎&‎، أما إن أُخذ وسيط عام كـ ‎auto&&‎، فسيكون مرجعًا أماميًا (forwarding reference) يشير إلى الوسيط المُمرّر، وليس مرجعًا يمينِيّا (rvalue reference):

auto lamb1 = [](int &&x) {return x + 5;};
auto lamb2 = [](auto &&x) {return x + 5;};
int x = 10;
lamb1(x);   
lamb2(x);   

في الشيفرة السابقة، لا تجوز (lamb1(x لوجوب استخدام std::move(x)‎ لأجل معامِلات &&int، بينما تجوز (lamb1(x لأن نوع x يُستنتج على أنه &int. كذلك يمكن أن تكون دوالّ لامدا متغايرة (variadic)، وأن تعيد توجيه وسائطها:

auto lam = [](auto&&... args){return f(std::forward<decltype(args)>(args)...);};

أو:

auto lam = [](auto&&... args){return f(decltype(args)(args)...);};

والتي لن تعمل "بالشكل الصحيح" إلّا مع المتغيّرات من نوع ‎auto&&‎. أيضًا، أحد الأسباب القوية لاستخدام دوال لامدا العامّة هو البنية اللغوية للزيارة (visiting syntax)، انظر:

boost::variant<int, double> value;
apply_visitor(value, [&](auto&& e){
std::cout << e;
});

هنا، قمنا هنا بالزيارة بأسلوب متعدد الأشكال، لكن في السياقات الأخرى فلا تهم أسماء النوع المُمرّرة:

mutex_wrapped<std::ostream&> os = std::cout;
os.write([&](auto&& os){
os << "hello world\n";
});

لا فائدة من تكرار النوع ‎std::ostream&‎ هنا؛ كأنك تذكر نوع المتغيّر في كل مرّة تستخدمه، ولقد أنشأنا هنا زائرًا غير متعدد الأشكال، كما استخدمنا ‎auto‎ هنا لنفس سبب استخدام ‎auto‎ في حلقة ‎for(:)‎.

استخدام دوال لامدا لفك حزم المعاملات المضمنة

الإصدار ≥ C++‎ 14

كان فكّ حزم المعامِلات (Parameter pack unpacking) يتطّلب كتابة دالّة مساعدة في كل مرّة تريد القيام بذلك. مثلًا:

template<std::size_t...Is >
    void print_indexes(std::index_sequence < Is... >)
    {
        using discard = int[];
        (void) discard
        {
            0, ((void)(
                std::cout << Is << '\n'    // هي ثابتة في وقت التصريف Is هنا
        ), 0)...
        };
    }

template<std::size_t I>
    void print_indexes_upto()
    {
        return print_indexes(std::make_index_sequence < I> {});
    }

يريد ‎print_indexes_upto‎ إنشاء وفكّ حزمة معاملات من الفهارس، ولفعل ذلك، يجب استدعاء دالّة مساعدة. يجب أن تنشئ دالة مساعدة مخصصة في كل مرة تريد فك حزمة معامِلات أنشأتها، ويمكن تجنب ذلك هنا باستخدام دوال لامدا، إذ يمكنك فكّ الحِزَم باستخدام دالة لامدا على النحو التالي:

template<std::size_t I>
    using index_t = std::integral_constant<std::size_t, I> ;
template<std::size_t I>
    constexpr index_t<I> index {};
template < class = void, std::size_t...Is >
    auto index_over(std::index_sequence < Is... >)
    {
        return[](auto && f)
        {
            using discard = int[];
            (void) discard
            {
                0, (void(
                    f(index < Is>)
            ), 0)...
            };
        };
    }

template<std::size_t N>
    auto index_over(index_t<N> = {})
    {
        return index_over(std::make_index_sequence < N> {});
    }

الإصدار ≥ C++‎ 17

يمكن تبسيط ‎index_over()‎ باستخدام التعبيرات المطوية على النحو التالي:

template<class=void, std::size_t...Is>
auto index_over( std::index_sequence<Is...> ) {
return [](auto&& f){
((void)(f(index<Is>)), ...);
};
}

بعد ذلك، يمكنك استخدام هذا لتجنّب الحاجة إلى فكّ حزم المعاملات يدويًا عبر تحميل زائد ثانٍ في الشيفرات الأخرى، فذلك سيتيح لك فكّ حزم المعاملات بشكل مضمّن (inline):

template < class Tup, class F>
    void for_each_tuple_element(Tup&& tup, F&& f) 
    {
        using T = std::remove_reference_t<Tup> ;
        using std::tuple_size;
        auto from_zero_to_N = index_over<tuple_size < T> {} > ();
        from_zero_to_N(
        [&](auto i)
            {
                using std::get;
                f(get<i> (std::forward<Tup> (tup)));
            }
    );
}

نوع ‎auto i‎ المُمرّر إلى دالة لامدا عبر ‎index_over‎ هو ‎std::integral_constant<std::size_t, ???>‎ الذي يحتوي على تحويل ‎constexpr‎ إلى std::size_t لا يعتمد على حالة this، وعليه نستطيع استخدامه كثابت في وقت التصريف، مثلا عندما نمرّره إلى ‎std::get<i>‎ أعلاه.

سنعيد الآن كتابة المثال أعلاه:

template<std::size_t I>
    void print_indexes_upto()
    {
        index_over(index < I>)([](auto i)
        {
            std::cout << i << '\n';    // هي ثابتة في وقت التصريف i هنا });
    }

صار المثال أقصر بكثير. انظر إن شئت هذا المثال الحيّ على ذلك.

الالتقاطات المعممة النوع Generalized capture

الإصدار ≥ C++‎ 14

تستطيع دوال لامدا التقاط التعبيرات وليس المتغيّرات فقط، هذا يسمح لدوال لامدا بتخزين أنواع النقل فقط (move-only types):

auto p = std::make_unique<T> (...);
auto lamb =[p = std::move(p)]()   
{
    p->SomeFunc();
};

هذا ينقل المتغيّر ‎p‎ الخارجي إلى متغيّر لامدا المُلتقط ، ويسمى أيضًا ‎p‎، وتمتلك ‎lamb‎ الآن الذاكرة المخصّصة لـ ‎make_unique‎. وبما أن التغليف (closure) يحتوي على نوع غير قابل للنسخ، فذلك يعني أنّ ‎lamb‎ ستكون غير قابلة للنسخ، لكن ستكون قابلة للنقل رغم هذا:

auto lamb_copy = lamb;    // غير جائز
auto lamb_move = std::move(lamb);    // جائز

الآن أصبحت ‎lamb_move‎ تملك الذاكرة، لاحظ أنّ ‎std::function<>‎ تتطّلب أن تكون القيم المخزّنة قابلة للنسخ، يمكنك كتابة دالة خاصّة بك تتطّلب النقل فقط أو وضع لامدا في غلاف مؤشّر مشترك ‎shared_ptr‎:

auto shared_lambda = [](auto&& f){
return [spf = std::make_shared<std::decay_t<decltype(f)>>(decltype(f)(f))]
(auto&&...args)->decltype(auto) {
return (*spf)(decltype(args)(args)...);
};
};
auto lamb_shared = shared_lambda(std::move(lamb_move));

هنا أخذنا دالة لامدا للنقل فقط ووضعنا حالتها في مؤشّر مشترك، ثم ستُعاد دالة لامدا قابلة للنسخ، ثم التخزين في ‎std::function‎ أو نحو ذلك.

يستخدم الالتقاط المُعمّم استنباط النوع ‎auto‎ لاستنباط نوع المتغيّر، وسيصرّح عن هذه الالتقاطات على أنّها قيم افتراضيًا، لكن يمكن أن تكون مراجع أيضًا:

int a = 0;
auto lamb = [&v = a](int add)   // مختلفان `a` و `v` تذكّر أنّ اسمَي
{
    v += add;    // `a` تعدي
};
lamb(20);    // ستصبح 20 `a` 

يستطيع الالتقاط المعمَّم أن يلتقط تعبيرًا عشوائيًا لكن لا يلزمه التقاط متغيرات خارجية:

auto lamb =[p = std::make_unique<T> (...)]()
{
    p->SomeFunc();
}

هذا مفيد في إعطاء دوال لامدا قيمًا عشوائية يمكنها الاحتفاظ بها وربّما تعديلها دون الحاجة إلى التصريح عنها خارجيًا، بالطبع، هذا لا يكون مفيدًا إلّا إذا كنت تنوي الوصول إلى تلك المتغيّرات بعد أن تكمل لامدا عملها.

التحويل إلى مؤشر دالة

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

auto sorter =[](int lhs, int rhs)->bool
{
    return lhs < rhs;
};
using func_ptr = bool(*)(int, int);
func_ptr sorter_func = sorter;    // تحويل ضمني

يمكن أيضًا فرض هذا التحويل باستخدام عامل "+" الأحادي:

func_ptr sorter_func2 = +sorter;    // فرض التحويل الضمني

سيكون سلوك استدعاء مؤشّر الدالّة هذا مكافئًا لاستدعاء ‎operator()‎ على لامدا، فلا يعتمد مؤشّر الدالّة على وجود مغلّف لامدا، لذلك قد يستمرّ حتى بعد انتهاء مغلّف لامدا. هذه الميزة مفيدة عند استخدام دوال لامدا مع الواجهات البرمجية التي تتعامل مع مؤشّرات الدوال، بدلاً من كائنات دوال C++‎.

الإصدار ≥ C++‎ 14

يمكن أيضًا تحويل دالة لامدا عامة لها قائمة التقاط فارغة إلى مؤشّر دالّة، وإذا لزم الأمر سيُستخدَم استنباط وسيط القالب (template argument deduction) لاختيار التخصيص الصحيح.

auto sorter =[](auto lhs, auto rhs)
{
    return lhs < rhs;
};
using func_ptr = bool(*)(int, int);
func_ptr sorter_func = sorter;    // int استنتاج
// لكن السطر التالي غامض
// func_ptr sorter_func2 = +sorter;

ترقية دوال لامدا إلى C++‎ 03 باستخدام الكائنات الدالية (functor)

دوال لامدا في C++‎ هي اختصارات مفيدة توفّر صياغة مختصرة لكتابة الدوال، ويمكن الحصول على وظيفة مشابهة في C++‎ 03 (وإن كانت مُطولًة) عن طريق تحويل دالّة لامدا إلى كائن دالّي:

// هذه بعض الأنواع
struct T1
{
    int dummy;
};
struct T2
{
    int dummy;
};
struct R
{
    int dummy;
};

هذه الشيفرة تستخدم دالة لامدا،هذا يعني أنها تستلزم C++11، نتابع المثال:

R use_lambda(T1 val, T2 ref)
{

هنا، استعمل auto لأن النوع المُعاد من دالة لامدا مجهول، نتابع:

    auto lambda =[val, &ref](int arg1, int arg2)->R
    {
        /* متن لامدا */
        return R();
    };
    return lambda(12, 27);
}

// C++03 صنف الكائن الدالي - صالح في
class Functor
{
    // قائمة الالتقاط
    T1 val;
    T2 &ref;
    public:
        // المنشئ
        inline Functor(T1 val, T2 &ref): val(val), ref(ref) {}

    // متن الكائن الدالي
    R operator()(int arg1, int arg2) const
    {
        /* متن لامدا */
        return R();
    }
};

هذا يكافئ use_lambda لكنه يستخدم كائنًا دالّيًا، وهو صالح في C++03:

R use_functor(T1 val, T2 ref)
{
    Functor functor(val, ref);
    return functor(12, 27);
}

int main()
{
    T1 t1;
    T2 t2;
    use_functor(t1, t2);
    use_lambda(t1, t2);
    return 0;
}

إذا كانت دالّة لامدا قابلة للتغيير (‎mutable‎)، فاجعل مُعامل الاستدعاء الخاص بالكائن الدالّي غير ثابت، أي:

R operator()(int arg1, int arg2) /*non-const*/
{
    /* متن لامدا */
    return R();
}

هذا الدرس جزء من سلسلة دروس عن C++‎.

ترجمة -بتصرّف- للفصل Chapter 73: Lambdas من كتاب C++ Notes for Professionals


تفاعل الأعضاء

أفضل التعليقات

لا توجد أية تعليقات بعد



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...