ما هو تعبير لامدا في C++ 11؟




lambda c++11 (6)

ما هو تعبير لامدا في C ++ 11؟ متى أستخدم واحدة؟ ما هو نوع المشكلة التي حلوها ولم يكن ذلك ممكنًا قبل تقديمها؟

بعض الأمثلة ، وحالات الاستخدام ستكون مفيدة.


المشكلة

يتضمن C ++ وظائف عامة مفيدة مثل std::for_each و std::transform ، والتي يمكن أن تكون مفيدة للغاية. لسوء الحظ ، يمكن أيضًا أن يكون الاستخدام مرهقًا جدًا ، خاصةً إذا كان functor الذي تود تطبيقه فريدًا بالنسبة إلى الوظيفة المعينة.

#include <algorithm>
#include <vector>

namespace {
  struct f {
    void operator()(int) {
      // do something
    }
  };
}

void func(std::vector<int>& v) {
  f f;
  std::for_each(v.begin(), v.end(), f);
}

إذا كنت تستخدم f مرة واحدة فقط وفي هذا المكان المحدد ، يبدو أنه من المبالغة أن تقوم بكتابة فصل كامل لفعل شيء تافه وواحد.

في C ++ 03 قد تميل إلى كتابة شيء مثل ما يلي ، للحفاظ على الفاحص المحلي:

void func2(std::vector<int>& v) {
  struct {
    void operator()(int) {
       // do something
    }
  } f;
  std::for_each(v.begin(), v.end(), f);
}

لكن هذا غير مسموح به ، لا يمكن تمرير f إلى وظيفة القالب في C ++ 03.

الحل الجديد

يقدم C ++ 11 lambdas تسمح لك بكتابة سطراً مضمنًا مجهولاً لاستبدال struct f . للحصول على أمثلة بسيطة صغيرة ، يمكن أن يكون ذلك أكثر نظافة للقراءة (يحافظ على كل شيء في مكان واحد) وربما أبسط للحفاظ عليه ، على سبيل المثال في أبسط أشكاله:

void func3(std::vector<int>& v) {
  std::for_each(v.begin(), v.end(), [](int) { /* do something here*/ });
}

وظائف لامبدا هي مجرد السكر النحوي للمغامرات المجهولة.

أنواع العودة

في حالات بسيطة ، يتم استنتاج نوع الإرجاع من lambda ، على سبيل المثال:

void func4(std::vector<double>& v) {
  std::transform(v.begin(), v.end(), v.begin(),
                 [](double d) { return d < 0.00001 ? 0 : d; }
                 );
}

ولكن عندما تبدأ بكتابة lambdas أكثر تعقيدًا ، فسوف تواجه بسرعة الحالات التي لا يمكن فيها استنتاج نوع الإرجاع من خلال المحول البرمجي ، على سبيل المثال:

void func4(std::vector<double>& v) {
    std::transform(v.begin(), v.end(), v.begin(),
        [](double d) {
            if (d < 0.0001) {
                return 0;
            } else {
                return d;
            }
        });
}

لحل هذه المشكلة ، يُسمح لك بتحديد نوع الإرجاع لوظيفة lambda ، باستخدام -> T :

void func4(std::vector<double>& v) {
    std::transform(v.begin(), v.end(), v.begin(),
        [](double d) -> double {
            if (d < 0.0001) {
                return 0;
            } else {
                return d;
            }
        });
}

متغيرات "التقاط"

حتى الآن ، لم نستخدم أي شيء آخر غير ما تم تمريره إلى اللمبادا داخله ، ولكن يمكننا أيضًا استخدام المتغيرات الأخرى ، داخل لامدا. إذا كنت تريد الوصول إلى متغيرات أخرى ، يمكنك استخدام عبارة الالتقاط ( [] من التعبير) ، والتي لم يتم استخدامها حتى الآن في هذه الأمثلة ، على سبيل المثال:

void func5(std::vector<double>& v, const double& epsilon) {
    std::transform(v.begin(), v.end(), v.begin(),
        [epsilon](double d) -> double {
            if (d < epsilon) {
                return 0;
            } else {
                return d;
            }
        });
}

يمكنك التقاط كل من المرجع والقيمة ، والتي يمكنك تحديدها باستخدام & و = على التوالي:

  • [&epsilon] التقاط بالإشارة
  • [&] يلتقط جميع المتغيرات المستخدمة في lambda حسب المرجع
  • [=] يلتقط جميع المتغيرات المستخدمة في لامدا من حيث القيمة
  • [&, epsilon] يلتقط متغيرات مثل مع & [&] ، ولكن epsilon حسب القيمة
  • [=, &epsilon] يلتقط المتغيرات مثل مع [=] ، ولكن epsilon بالرجوع

operator() الذي تم إنشاؤه operator() هو const بشكل افتراضي ، مع وجود ضمنية أن الالتقاط سيكون const عند الوصول إليها بشكل افتراضي. هذا له تأثير على أن كل مكالمة بنفس المدخلات ستنتج نفس النتيجة ، ولكن يمكنك وضع علامة على lambda mutable لطلب أن operator() الذي يتم إنتاجه ليس const .


ما هي وظيفة امدا؟

ينشأ مفهوم C ++ لوظيفة لامدا في حساب lambda والبرنامج الوظيفي. إن lambda دالة غير مسمى مفيدة (في البرمجة الفعلية ، وليس نظرية) للقصاصات القصيرة من الشفرة التي من المستحيل إعادة استخدامها ولا تساوي التسمية.

في C ++ يتم تعريف وظيفة lambda مثل هذا

[]() { } // barebone lambda

أو في كل مجدها

[]() mutable -> T { } // T is the return type, still lacking throw()

[] هي قائمة الالتقاط و () قائمة الوسيطة و {} جسد الوظيفة.

قائمة القبض

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

  1. قيمة: [x]
  2. مرجع [& x]
  3. أي متغير حاليًا في النطاق حسب المرجع [&]
  4. نفس 3 ، ولكن حسب القيمة [=]

يمكنك مزج أي مما سبق في قائمة مفصولة بفواصل [x, &y] .

قائمة الوسيطة

قائمة الوسيطة هي نفسها كما في أي دالة C ++ أخرى.

هيئة الوظيفة

الرمز الذي سيتم تنفيذه عند استدعاء lambda فعليًا.

العودة نوع الخصم

إذا كان لـ lambda بيان إرجاع واحد فقط ، يمكن حذف نوع الإرجاع ويكون له النوع الضمني من النوع decltype(return_statement) .

متقلب

إذا تم وضع علامة على lambda mutable (على سبيل المثال []() mutable { } ) ، فيسمح لها بتحوير القيم التي تم التقاطها حسب القيمة.

استخدم حالات

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

C ++ 14

في C ++ 14 تم تمديد lambdas من خلال المقترحات المختلفة.

بادئة لامبادا

يمكن الآن تهيئة عنصر قائمة الالتقاط بـ = . هذا يسمح بإعادة تسمية المتغيرات والتقاطها عن طريق الحركة. مثال مأخوذ من المعيار:

int x = 4;
auto y = [&r = x, x = x+1]()->int {
            r += 2;
            return x+2;
         }();  // Updates ::x to 6, and initializes y to 7.

وأخرى مأخوذة من ويكيبيديا تبين كيفية التقاطها مع std::move :

auto ptr = std::make_unique<int>(10); // See below for std::make_unique
auto lambda = [ptr = std::move(ptr)] {return *ptr;};

عام لامداس

يمكن أن يكون Lambdas الآن عامًا (سيكون auto مساوياً لـ T هنا إذا كانت T عبارة عن وسيطة قالب نمط في مكان ما في النطاق المحيط):

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

تحسين نوع المرتجعات

يسمح C ++ 14 باسترجاع أنواع الإرجاع لكل دالة ولا يحدها إلى وظائف return expression; النموذج return expression; . يتم تمديد هذا أيضا إلى lambdas.


تستخدم تعبيرات لامدا عادة لتغليف الخوارزميات بحيث يمكن تمريرها إلى وظيفة أخرى. ومع ذلك ، من الممكن تنفيذ lambda فور التعريف :

[&](){ ...your code... }(); // immediately executed lambda expression

يعادل وظيفيا ل

{ ...your code... } // simple code block

هذا يجعل تعبيرات لامدا أداة قوية لإعادة بناء الوظائف المعقدة . تبدأ من خلال لف جزء التعليمات البرمجية في وظيفة lambda كما هو موضح أعلاه. ويمكن بعد ذلك إجراء عملية وضع المعلمات الصريحة تدريجياً باستخدام اختبار وسيط بعد كل خطوة. بمجرد أن تقوم بتجميد شفرة التعليمات البرمجية بالكامل (كما هو موضح من خلال إزالة علامة & ) ، يمكنك نقل الشفرة إلى موقع خارجي وجعلها وظيفة عادية.

وبالمثل ، يمكنك استخدام تعبيرات لامدا لتهيئة المتغيرات بناء على نتيجة خوارزمية ...

int a = []( int b ){ int r=1; while (b>0) r*=b--; return r; }(5); // 5!

كطريقة لتقسيم منطق البرنامج الخاص بك ، قد تجد أنه من المفيد تمرير تعبير لامدا كحجة لتعبير لامدا آخر ...

[&]( std::function<void()> algorithm ) // wrapper section
   {
   ...your wrapper code...
   algorithm();
   ...your wrapper code...
   }
([&]() // algorithm section
   {
   ...your algorithm code...
   });

تتيح لك تعبيرات Lambda إنشاء وظائف متداخلة مسماة ، والتي يمكن أن تكون طريقة ملائمة لتجنب تكرار المنطق. استخدام لامبداس المسمى أيضا يميل إلى أن يكون أسهل قليلا على العينين (بالمقارنة مع lambdas مضمنة المجهول) عند تمرير وظيفة غير تافهة كمعلمة إلى وظيفة أخرى. ملاحظة: لا تنس الفاصلة المنقوطة بعد قوس الإغلاق المجعد.

auto algorithm = [&]( double x, double m, double b ) -> double
   {
   return m*x+b;
   };

int a=algorithm(1,2,3), b=algorithm(4,5,6);

إذا كشف التشكيل الجانبي اللاحق عن مقدار كبير من التهيئة الزائدة لكائن الدالة ، فقد تختار إعادة كتابته كدالة عادية.


حسنًا ، أحد الاستخدامات العملية التي اكتشفتها هو تقليل كود لوحة الغلاية. فمثلا:

void process_z_vec(vector<int>& vec)
{
  auto print_2d = [](const vector<int>& board, int bsize)
  {
    for(int i = 0; i<bsize; i++)
    {
      for(int j=0; j<bsize; j++)
      {
        cout << board[bsize*i+j] << " ";
      }
      cout << "\n";
    }
  };
  // Do sth with the vec.
  print_2d(vec,x_size);
  // Do sth else with the vec.
  print_2d(vec,y_size);
  //... 
}

بدون لامدا ، قد تحتاج إلى القيام بشيء لحالات bsize مختلفة. بالطبع يمكنك إنشاء وظيفة ولكن ماذا لو كنت ترغب في الحد من الاستخدام في نطاق وظيفة مستخدم الروح؟ طبيعة lambda يفي بهذا الشرط وأستخدمه في هذه الحالة.


وظيفة lambda هي وظيفة مجهولة تقوم بإنشائها في سطر. يمكنه التقاط المتغيرات كما أوضحها البعض (على سبيل المثال http://www.stroustrup.com/C++11FAQ.html#lambda ) ولكن هناك بعض القيود. على سبيل المثال ، إذا كانت هناك واجهة اتصال مثل هذا ،

void apply(void (*f)(int)) {
    f(10);
    f(20);
    f(30);
}

يمكنك كتابة وظيفة على الفور لاستخدامها مثل تلك التي تم تمريرها للتطبيق أدناه:

int col=0;
void output() {
    apply([](int data) {
        cout << data << ((++col % 10) ? ' ' : '\n');
    });
}

لكن لا يمكنك فعل هذا:

void output(int n) {
    int col=0;
    apply([&col,n](int data) {
        cout << data << ((++col % 10) ? ' ' : '\n');
    });
}

بسبب القيود في معيار C ++ 11. إذا كنت تريد استخدام الالتقاطات ، فعليك الاعتماد على المكتبة و

#include <functional> 

(أو بعض مكتبة STL الأخرى مثل الخوارزمية للحصول عليها بشكل غير مباشر) ثم تعمل مع الدالة std :: بدلاً من تمرير الوظائف العادية كمعلمات مثل:

#include <functional>
void apply(std::function<void(int)> f) {
    f(10);
    f(20);
    f(30);
}
void output(int width) {
    int col;
    apply([width,&col](int data) {
        cout << data << ((++col % width) ? ' ' : '\n');
    });
}

الأجوبة

س: ما هو تعبير لامدا في C ++ 11؟

ج: تحت غطاء المحرك ، فإن الغرض من فئة autogenerated مع consting المشغل () const . يسمى هذا الكائن بإغلاق ويتم إنشاؤه بواسطة برنامج التحويل البرمجي. يقترب مفهوم "الإغلاق" هذا من مفهوم الربط من C ++ 11. لكن اللامداس عادة ما تولد كود أفضل. وتسمح المكالمات من خلال الإغلاق بالتضمين الكامل.

س: متى أستخدم واحدة؟

ج: لتحديد "المنطق البسيط والصغير" واطلب من المترجم توليد جيل من السؤال السابق. يمكنك إعطاء مترجم بعض التعبيرات التي تريد أن تكون داخل المشغل (). جميع مترجم الاشياء الأخرى سوف تولد لك.

س: ما هو نوع المشكلة التي حلوها ولم يكن ذلك ممكنًا قبل تقديمها؟

ج: إنه نوع من السكر في تركيب الجمل مثل مشغلي الحمولة الزائدة بدلاً من وظائف عمليات إضافة subrtact المخصصة ... ولكنه يوفر المزيد من الأسطر من التعليمات البرمجية غير الضرورية لفك 1-3 خطوط من المنطق الحقيقي لبعض الطبقات ، وما إلى ذلك! يعتقد بعض المهندسين أنه إذا كان عدد الخطوط أصغر ، فهناك فرصة أقل لحدوث أخطاء فيه (أعتقد ذلك أيضًا)

مثال على الاستخدام

auto x = [=](int arg1){printf("%i", arg1); };
void(*f)(int) = x;
f(1);
x(1);

إضافات حول lambdas ، لا تغطيها السؤال. تجاهل هذا القسم إذا لم تكن مهتمًا

1. القيم الملتقطة. ما يمكنك التقاطه

1.1. يمكنك الرجوع إلى متغير مع مدة التخزين الثابتة في lamdas. يتم القبض عليهم جميعا.

1.2. يمكنك استخدام lamda لقيم الالتقاط "حسب القيمة". في مثل هذه الحالة ، سيتم نسخ الفؤرة التي تم التقاطها إلى كائن الدالة (الإغلاق).

[captureVar1,captureVar2](int arg1){}

1.3. يمكنك التقاط تكون مرجعا. & - في هذا السياق يعني الإشارة ، وليس المؤشرات.

   [&captureVar1,&captureVar2](int arg1){}

1.4. إنه موجود تدوين لالتقاط كافة vars غير ثابتة حسب القيمة أو حسب المرجع

  [=](int arg1){} // capture all not-static vars by value

  [&](int arg1){} // capture all not-static vars by reference

1.5. إنه موجود لتدوين كل الفؤر غير الساكنة من حيث القيمة ، أو بالرجوع وتحديد الشيئ. أكثر من. أمثلة: التقاط جميع الفؤر غير الساكنة حسب القيمة ، ولكن عن طريق التقاط إشارة Param2

[=,&Param2](int arg1){} 

القبض على جميع غير ثابتة vars بالرجوع إليها ، ولكن عن طريق التقاط قيمة Param2

[&,Param2](int arg1){} 

2. خصم نوع العودة

2.1. يمكن استنتاج نوع عودة لامدا إذا كان لامدا عبارة واحدة. أو يمكنك تحديدها بوضوح.

[=](int arg1)->trailing_return_type{return trailing_return_type();}

إذا كان لـ lambda أكثر من تعبير واحد ، فيجب تحديد نوع الإرجاع عبر نوع الإرجاع الخلفي. أيضا يمكن تطبيق تركيب مماثل على وظائف السيارات ووظائف الأعضاء

3. القيم الملتقطة. ما لا يمكنك التقاطه

3.1. يمكنك التقاط vars المحلي فقط ، وليس متغير العضو من الكائن.

4. Сonversions

4.1. lambda ليس مؤشر دالة وهو ليس دالة مجهولة ، ولكن يمكن تحويله ضمنيًا إلى مؤشر وظيفة.

ملاحظة

  1. يمكن العثور على المزيد من المعلومات حول lambda grammar في مسودة العمل الخاصة بلغة البرمجة C ++ # 337، 2012-01-16، 5.1.2. Lambda Expressions، p.88

  2. في C ++ 14 تمت إضافة الميزة الإضافية التي سميت باسم "init capture". انها تسمح لأداء إعلان إعتباطي من أعضاء البيانات إغلاق:

    auto toFloat = [](int value) { return float(value);};
    auto interpolate = [min = toFloat(0), max = toFloat(255)](int value)->float { return (value - min) / (max - min);};
    




c++-faq