Lambdas: من C ++ 11 إلى C ++ 20. الجزء 1

مساء الخير أيها الأصدقاء. لقد أعددنا لك اليوم ترجمة للجزء الأول من المقال "Lambdas: من C ++ 11 إلى C ++ 20" . تم توقيت نشر هذه المادة لتتزامن مع إطلاق الدورة التدريبية "C ++ Developer" ، والتي تبدأ غدًا.

تعد تعبيرات Lambda واحدة من أقوى الإضافات في C ++ 11 وتستمر في التطور مع كل معيار لغة جديد. في هذه المقالة ، سوف نتعرف على تاريخهم ونلقي نظرة على تطور هذا الجزء المهم من C ++ الحديث.



الجزء الثاني متاح هنا:
Lambdas: من C ++ 11 إلى C ++ 20 ، الجزء 2

دخول

في اجتماع محلي لمجموعة C ++ ، كان لدينا جلسة برمجة مباشرة حول "تاريخ" تعبيرات lambda. قاد المحادثة خبير C ++ ، Tomasz Kamiński ( انظر ملف تعريف Linkedin الخاص بـ Thomas ). هنا هو الحدث:

Lambdas: من C ++ 11 إلى C ++ 20 - C ++ User Group Krakow

قررت أن تأخذ الرمز من توماس (بإذنه!) ، صفه وإنشاء مقالة منفصلة.

سنبدأ باستكشاف C ++ 03 والحاجة إلى تعبيرات وظيفية محلية مضغوطة. ثم ننتقل إلى C ++ 11 و C ++ 14. في الجزء الثاني من السلسلة ، سنرى تغييرات في C ++ 17 وحتى نلقي نظرة على ما سيحدث في C ++ 20.

Lambdas في C ++ 03

من البداية ، يمكن أن تأخذ std::algorithms ، مثل std::sort ، أي كائن يسمى وتطلق عليه عناصر الحاوية. ومع ذلك ، في C ++ 03 ، تضمن هذا مؤشرات فقط إلى الوظائف والمُعامِلات.

على سبيل المثال:

 #include <iostream> #include <algorithm> #include <vector> struct PrintFunctor { void operator()(int x) const { std::cout << x << std::endl; } }; int main() { std::vector<int> v; v.push_back(1); v.push_back(2); std::for_each(v.begin(), v.end(), PrintFunctor()); } 

رمز التشغيل: Wandbox

لكن المشكلة تكمن في أنه كان عليك كتابة وظيفة منفصلة أو عامل توجيه في نطاق مختلف ، وليس في نطاق استدعاء الخوارزمية.

كحل محتمل ، قد تفكر في كتابة فصل دراسي محلي - لأن C ++ يدعم بناء الجملة هذا دائمًا. لكنها لا تعمل ...

ألقِ نظرة على هذا الكود:

 int main() { struct PrintFunctor { void operator()(int x) const { std::cout << x << std::endl; } }; std::vector<int> v; v.push_back(1); v.push_back(2); std::for_each(v.begin(), v.end(), PrintFunctor()); } 

حاول تجميعه مع -std=c++98 وسترى الخطأ التالي في GCC:

 error: template argument for 'template<class _IIter, class _Funct> _Funct std::for_each(_IIter, _IIter, _Funct)' uses local type 'main()::PrintFunctor' 

بشكل أساسي ، في C ++ 98/03 ، لا يمكنك إنشاء مثيل لقالب بنوع محلي.
نظرًا لكل هذه القيود ، بدأت اللجنة في تطوير ميزة جديدة يمكننا أن نطلقها ونطلق عليها "في المكان" ... "تعبيرات lambda"!

إذا نظرنا إلى N3337 - الإصدار الأخير من C ++ 11 ، فسنرى قسمًا منفصلًا عن lambdas: [expr.prim.lambda] .

بجانب C ++ 11

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

فيما يلي مثال لرمز أساسي يُظهر أيضًا كائن functor المحلي المطابق:

 #include <iostream> #include <algorithm> #include <vector> int main() { struct { void operator()(int x) const { std::cout << x << '\n'; } } someInstance; std::vector<int> v; v.push_back(1); v.push_back(2); std::for_each(v.begin(), v.end(), someInstance); std::for_each(v.begin(), v.end(), [] (int x) { std::cout << x << '\n'; } ); } 

مثال: WandBox

يمكنك أيضًا التحقق من CppInsights ، والذي يوضح كيفية قيام المترجم بتمديد الكود:

ألقِ نظرة على هذا المثال:

CppInsighs: اختبار لامدا

في هذا المثال ، يحول المترجم:

 [] (int x) { std::cout << x << '\n'; } 


في شيء مشابه لهذا (شكل مبسط):

 struct { void operator()(int x) const { std::cout << x << '\n'; } } someInstance; 

بناء جملة التعبير Lambda:

 [] () { ; } ^ ^ ^ | | | | | : mutable, exception, trailing return, ... | | |   |      

بعض التعاريف قبل أن نبدأ:

من [expr.prim.lambda # 2] :

تقييم تعبير lambda ينتج عنه قيمة مؤقتة. يسمى هذا الكائن المؤقت كائن إغلاق .

ومن [expr.prim.lambda # 3] :

نوع تعبير lambda (والذي هو أيضًا نوع كائن الإغلاق) هو نوع فريد من نوعه بدون اتحاد من الفئة يسمى نوع الإغلاق .

بعض الأمثلة على تعبيرات لامدا:

على سبيل المثال:

 [](float f, int a) { return a*f; } [](MyClass t) -> int { auto a = t.compute(); return a; } [](int a, int b) { return a < b; } 

نوع لامدا

نظرًا لأن المترجم يولد اسمًا فريدًا لكل لامدا ، لا يمكن معرفة ذلك مقدمًا.

 auto myLambda = [](int a) -> double { return 2.0 * a; } 

علاوة على ذلك [expr.prim.lambda] :
يحتوي نوع الإغلاق المرتبط بتعبير lambda على مُنشئ افتراضي عن بُعد ([dcl.fct.def.delete]) ومشغل تعيين عن بُعد.

لذلك ، لا يمكنك الكتابة:

 auto foo = [&x, &y]() { ++x; ++y; }; decltype(foo) fooCopy; 

سيؤدي ذلك إلى حدوث الخطأ التالي في GCC:

 error: use of deleted function 'main()::<lambda()>::<lambda>()' decltype(foo) fooCopy; ^~~~~~~ note: a lambda closure type has a deleted default constructor 

استدعاء المشغل

تتم ترجمة الكود الذي تضعه في نص lambda إلى رمز المشغل () من نوع الإغلاق المقابل.

بشكل افتراضي ، هذه طريقة ثابتة مضمنة. يمكنك تغييره عن طريق تحديد mutable بعد إعلان المعلمات:

 auto myLambda = [](int a) mutable { std::cout << a; } 

على الرغم من أن الطريقة الثابتة ليست "مشكلة" لامدا دون وجود قائمة التقاط فارغة ... إنها مهمة عندما تريد التقاط شيء ما.

أسر

[] لا يقدم lambda فحسب ، بل يحتوي أيضًا على قائمة بالمتغيرات التي تم التقاطها. وهذا ما يسمى قائمة القبض.

عن طريق التقاط متغير ، يمكنك إنشاء عضو نسخة من هذا المتغير في نوع الإغلاق. ثم داخل جسم لامدا يمكنك الوصول إليه.

بناء الجملة الأساسي هو:

  • [&] - التقاط حسب المرجع ، يتم الإعلان عن نطاق جميع المتغيرات في التخزين التلقائي
  • [=] - الالتقاط حسب القيمة ، يتم نسخ القيمة
  • [x، & y] - يلتقط x بشكل صريح بالقيمة ، و y بالرجوع إليها

على سبيل المثال:

 int x = 1, y = 1; { std::cout << x << " " << y << std::endl; auto foo = [&x, &y]() { ++x; ++y; }; foo(); std::cout << x << " " << y << std::endl; } 

يمكنك اللعب مع المثال الكامل هنا: Wandbox

على الرغم من أن تحديد [=] أو [&] يمكن أن يكون ملائمًا - نظرًا لأنه يلتقط جميع المتغيرات في التخزين التلقائي ، إلا أنه من الواضح التقاط المتغيرات بوضوح. وبالتالي ، يمكن للمترجم أن يحذرك من التأثيرات غير المرغوب فيها (انظر ، على سبيل المثال ، الملاحظات على المتغيرات العامة والثابتة)

يمكنك أيضًا قراءة المزيد في الفقرة 31 من Effective Modern C ++ بواسطة Scott Meyers: "تجنب أوضاع الالتقاط الافتراضية".

واقتباس مهم:
إغلاق C ++ لا تزيد من عمر الارتباطات الملتقطة.


متقلب

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

 int x = 1, y = 1; std::cout << x << " " << y << std::endl; auto foo = [x, y]() mutable { ++x; ++y; }; foo(); std::cout << x << " " << y << std::endl; 

في المثال أعلاه ، يمكننا تغيير قيم x و y ... لكن هذه ليست سوى نسخ x و y من النطاق المرفق.

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

إذا كان لديك قيمة عمومية ثم استخدمت [=] في lambda ، فقد تعتقد أن القيمة العامة يتم التقاطها أيضًا من خلال القيمة ... ولكنها ليست كذلك.

 int global = 10; int main() { std::cout << global << std::endl; auto foo = [=] () mutable { ++global; }; foo(); std::cout << global << std::endl; [] { ++global; } (); std::cout << global << std::endl; [global] { ++global; } (); } 

يمكنك اللعب باستخدام الكود هنا: Wandbox

يتم التقاط المتغيرات فقط في التخزين التلقائي. قد يصدر مجلس التعاون الخليجي التحذير التالي:

 warning: capture of variable 'global' with non-automatic storage duration 

سيظهر هذا التحذير فقط إذا قمت بالتقاط المتغير العام بشكل صريح ، لذلك إذا كنت تستخدم [=] ، فلن يساعدك برنامج التحويل البرمجي.
المحول البرمجي Clang مفيد أكثر لأنه ينشئ خطأ:

 error: 'global' cannot be captured because it does not have automatic storage duration 

انظر Wandbox

التقاط متغيرات ثابتة

التقاط المتغيرات الثابتة يشبه الالتقاط العام:

 #include <iostream> void bar() { static int static_int = 10; std::cout << static_int << std::endl; auto foo = [=] () mutable { ++static_int; }; foo(); std::cout << static_int << std::endl; [] { ++static_int; } (); std::cout << static_int << std::endl; [static_int] { ++static_int; } (); } int main() { bar(); } 

يمكنك اللعب باستخدام الكود هنا: Wandbox

الاستنتاج:

 10 11 12 

ومرة أخرى ، سوف يظهر تحذير فقط إذا قمت بالتقاط صراحة متغير ثابت ، لذلك إذا كنت تستخدم [=] ، فلن يساعدك برنامج التحويل البرمجي.

القبض على عضو الصف

هل تعرف ما يحدث بعد تنفيذ الكود التالي:

 #include <iostream> #include <functional> struct Baz { std::function<void()> foo() { return [=] { std::cout << s << std::endl; }; } std::string s; }; int main() { auto f1 = Baz{"ala"}.foo(); auto f2 = Baz{"ula"}.foo(); f1(); f2(); } 

رمز تعلن كائن Baz ثم المكالمات foo() . لاحظ أن foo() تُرجع lambda (المخزنة في std::function ) التي تلتقط عضوًا من الفصل.

نظرًا لأننا نستخدم كائنات مؤقتة ، لا يمكننا التأكد من ما سيحدث عند استدعاء f1 و f2. هذه مشكلة ارتباط يتدلى يؤدي سلوك غير معروف.

وبالمثل:

 struct Bar { std::string const& foo() const { return s; }; std::string s; }; auto&& f1 = Bar{"ala"}.foo(); //   

لعب مع رمز Wandbox

مرة أخرى ، إذا حددت الالتقاط بشكل صريح ([صور]):

 std::function<void()> foo() { return [s] { std::cout << s << std::endl; }; } 

المترجم سيمنع الخطأ الخاص بك:

 In member function 'std::function<void()> Baz::foo()': error: capture of non-variable 'Baz::s' error: 'this' was not captured for this lambda function ... 

انظر مثال: Wandbox

كائنات قادرة على التحرك فقط

إذا كان لديك كائن لا يمكن نقله (على سبيل المثال ، unique_ptr) ، فلا يمكنك وضعه في لامدا كمتغير تم التقاطه. لا يعمل الالتقاط حسب القيمة ، لذا لا يمكنك التقاطه إلا بالرجوع إليه ... ومع ذلك ، لن ينقل هذا التحويل إليك ، وربما هذا ليس هو ما تريده.

 std::unique_ptr<int> p(new int[10]); auto foo = [p] () {}; //  .... 

إنقاذ الثوابت

إذا قمت بالتقاط متغير ثابت ، فسيتم الحفاظ على الثبات:

 int const x = 10; auto foo = [x] () mutable { std::cout << std::is_const<decltype(x)>::value << std::endl; x = 11; }; foo(); 

انظر الكود:

نوع العودة

في C ++ 11 ، يمكنك تخطي trailing نوع الإرجاع lambda ، ومن ثم يقوم المحول البرمجي بإخراجه لك.

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

راجع تقارير C ++ القياسية عن عيوب اللغة والمشكلات المقبولة (شكرًا لتوماس للعثور على الرابط الصحيح!)

وبالتالي ، بدءًا من الإصدار C ++ 11 ، يمكن لبرنامج التحويل البرمجي استنتاج نوع قيمة الإرجاع إذا كان يمكن تحويل جميع عبارات الإرجاع إلى نفس النوع.
إذا كانت كل عبارات الإرجاع تُرجع التعبير وأنواع الإرجاع بعد تحويل القيمة إلى قيمة rvalue (7.1 [conv.lval]) والصفيف إلى المؤشر (7.2 [conv.array]) والدالة إلى المؤشر (7.3 [conv. func]) هو نفس النوع العام ؛
 auto baz = [] () { int x = 10; if ( x < 20) return x * 1.1; else return x * 2.1; }; 

يمكنك اللعب باستخدام الكود هنا: Wandbox

هناك return في lambda أعلاه ، لكنهم جميعا يشيرون إلى double ، لذلك يمكن للمترجم أن يستنتج النوع.

IIFE - استدعاء دالة فورًا التعبير

في الأمثلة الخاصة بنا ، قمت بتعريف lambda ثم سميته باستخدام كائن الإغلاق ... ولكن يمكن أيضًا استدعاؤه على الفور:

 int x = 1, y = 1; [&]() { ++x; ++y; }(); // <-- call () std::cout << x << " " << y << std::endl; 

يمكن أن يكون مثل هذا التعبير مفيدًا في التهيئة المعقدة للكائنات الثابتة.

 const auto val = []() { /*   ... */ }(); 

لقد كتبت المزيد حول هذا الموضوع في IIFE لمرحلة التهيئة المعقدة .

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

على سبيل المثال:

 #include <iostream> void callWith10(void(* bar)(int)) { bar(10); } int main() { struct { using f_ptr = void(*)(int); void operator()(int s) const { return call(s); } operator f_ptr() const { return &call; } private: static void call(int s) { std::cout << s << std::endl; }; } baz; callWith10(baz); callWith10([](int x) { std::cout << x << std::endl; }); } 

يمكنك اللعب باستخدام الكود هنا: Wandbox

تحسينات في C ++ 14

N4140 القياسية ولامدا : [expr.prim.lambda] .

أضاف C ++ 14 تحسينين مهمين إلى تعبيرات lambda:

  • يلتقط مع مهيئ
  • لامدا المشتركة

هذه الميزات حل العديد من المشاكل التي كانت مرئية في C ++ 11.

نوع العودة

تم تحديث إخراج نوع القيمة المرتدة من تعبير lambda ليتوافق مع قواعد الإخراج التلقائي للوظائف.

[expr.prim.lambda # 4]
نوع الإرجاع من lambda هو تلقائي ، يتم استبداله بنوع الإرجاع الخلفي ، إذا تم توفيره و / أو استنتاجه من عبارات الإرجاع ، كما هو موضح في [dcl.spec.auto].
يلتقط مع مهيئ

باختصار ، يمكننا إنشاء متغير عضو جديد من نوع الإغلاق ثم استخدامه داخل تعبير lambda.

على سبيل المثال:

 int main() { int x = 10; int y = 11; auto foo = [z = x+y]() { std::cout << z << '\n'; }; foo(); } 

يمكن أن يحل هذا العديد من المشكلات ، على سبيل المثال ، مع الأنواع المتوفرة فقط للتنقل.

الإزاحة

الآن يمكننا نقل الكائن إلى عضو من نوع الإغلاق:

 #include <memory> int main() { std::unique_ptr<int> p(new int[10]); auto foo = [x=10] () mutable { ++x; }; auto bar = [ptr=std::move(p)] {}; auto baz = [p=std::move(p)] {}; } 

الأمثل

فكرة أخرى هي استخدامه كأسلوب تحسين محتمل. بدلاً من حساب بعض القيمة في كل مرة نسميها lambda ، يمكننا حسابها مرة واحدة في المُهيئ:

 #include <iostream> #include <algorithm> #include <vector> #include <memory> #include <iostream> #include <string> int main() { using namespace std::string_literals; std::vector<std::string> vs; std::find_if(vs.begin(), vs.end(), [](std::string const& s) { return s == "foo"s + "bar"s; }); std::find_if(vs.begin(), vs.end(), [p="foo"s + "bar"s](std::string const& s) { return s == p; }); } 

التقاط متغير عضو

يمكن أيضًا استخدام أداة تهيئة لالتقاط متغير عضو. ثم يمكننا الحصول على نسخة من متغير العضو ولا تقلق بشأن الروابط المتدلية.

على سبيل المثال:

 struct Baz { auto foo() { return [s=s] { std::cout << s << std::endl; }; } std::string s; }; int main() { auto f1 = Baz{"ala"}.foo(); auto f2 = Baz{"ula"}.foo(); f1(); f2(); } 

يمكنك اللعب باستخدام الكود هنا: Wandbox


في foo() نلتقط متغير العضو بنسخه إلى نوع الإغلاق. بالإضافة إلى ذلك ، نحن نستخدم auto لإخراج الطريقة بالكامل (سابقًا ، في C ++ 11 ، يمكننا استخدام std::function ).

تعبيرات لامدا العامة

تحسن كبير آخر هو لامدا المعمم.
بدءًا من الإصدار C ++ 14 ، يمكنك الكتابة:

 auto foo = [](auto x) { std::cout << x << '\n'; }; foo(10); foo(10.1234); foo("hello world"); 

هذا يكافئ استخدام إعلان قالب في بيان مكالمة من نوع الإغلاق:

 struct { template<typename T> void operator()(T x) const { std::cout << x << '\n'; } } someInstance; 

مثل هذا اللمبا المعمم يمكن أن يكون مفيدًا جدًا عندما يكون من الصعب استنتاج نوع ما.

على سبيل المثال:

 std::map<std::string, int> numbers { { "one", 1 }, {"two", 2 }, { "three", 3 } }; //      pair<const string, int>! std::for_each(std::begin(numbers), std::end(numbers), [](const std::pair<std::string, int>& entry) { std::cout << entry.first << " = " << entry.second << '\n'; } ); 

هل أنا مخطئ هنا؟ هل يحتوي الإدخال على النوع الصحيح؟
.
.
.
ربما لا ، نظرًا لأن نوع القيمة std :: map هو std::pair<const Key, T> . لذلك فإن الكود الخاص بي سيقوم بعمل نسخ إضافية من الخطوط ...
يمكن إصلاح هذا مع auto :

 std::for_each(std::begin(numbers), std::end(numbers), [](auto& entry) { std::cout << entry.first << " = " << entry.second << '\n'; } ); 

يمكنك اللعب باستخدام الكود هنا: Wandbox

استنتاج

يا لها من قصة!

في هذه المقالة ، بدأنا من الأيام الأولى لتعبيرات lambda في C ++ 03 و C ++ 11 وانتقلنا إلى إصدار محسّن في C ++ 14.

لقد رأيت كيفية إنشاء لامدا ، وما هو الهيكل الأساسي لهذا التعبير ، وما هي قائمة الالتقاط ، وأكثر من ذلك بكثير.

في الجزء التالي من المقالة ، سننتقل إلى C ++ 17 والتعرف على الميزات المستقبلية لـ C ++ 20.

الجزء الثاني متاح هنا:

Lambdas: من C ++ 11 إلى C ++ 20 ، الجزء 2


مراجع

C ++ 11 - [expr.prim.lambda]
C ++ 14 - [expr.prim.lambda]
تعبيرات Lambda في C ++ | مستندات مايكروسوفت
إزالة الغموض عن C ++ lambdas - مثقاب مثبت - بدعم من Feabhas ؛ مثبت مثقاب - بدعم من Feabhas


نحن في انتظار تعليقاتكم ودعوة جميع المهتمين في الدورة التدريبية "C ++ Developer" .

Source: https://habr.com/ru/post/ar444524/


All Articles