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

مرحبًا يا هابروفسك. فيما يتعلق ببدء التوظيف في مجموعة جديدة في الدورة التدريبية "C ++ Developer" ، فإننا نشارك معك ترجمة الجزء الثاني من المقالة "Lambdas: من C ++ 11 إلى C ++ 20". الجزء الأول يمكن قراءته هنا .



في الجزء الأول من السلسلة ، نظرنا إلى lambdas من حيث C ++ 03 و C ++ 11 و C ++ 14. في هذه المقالة ، وصفت الدوافع الكامنة وراء ميزة C ++ القوية هذه ، والاستخدام الأساسي ، وبناء الجملة ، والتحسينات في كل من معايير اللغة. ذكرت أيضا بعض الحالات الحدودية.
حان الوقت الآن للانتقال إلى C ++ 17 وإلقاء نظرة على المستقبل (قريب جدًا!): C ++ 20.

دخول

تذكير قليل: جاءت فكرة هذه السلسلة بعد أحد اجتماعات مجموعة مستخدمي C ++ الأخيرة في كراكوف.

كان لدينا جلسة برمجة حية حول "تاريخ" تعبيرات لامدا. أجرى المحادثة توماس كامينسكي الخبير في برنامج C ++ ( انظر ملف تعريف Linkedin الخاص بـ Thomas ). هنا هو الحدث:
Lambdas: من C ++ 11 إلى C ++ 20 - C ++ User Group Krakow .

قررت أخذ الرمز من توماس (بإذنه!) وكتابة مقالات بناءً عليه. في الجزء الأول من السلسلة تحدثت عن تعبيرات لامدا على النحو التالي:

  • بناء الجملة الأساسية
  • نوع لامدا
  • استدعاء المشغل
  • التقاط المتغيرات (متغيرات قابلة للتغيير وعالمية وثابتة وأعضاء الفصل وهذا المؤشر ، كائنات قابلة للحركة فقط ، وتخزين ثوابت):

    • نوع العودة
    • IIFE - استدعاء دالة فورًا التعبير
    • التحويل إلى مؤشر وظيفة
    • نوع العودة
    • IIFE - استحضرت التعبيرات فورًا
    • تحويل إلى مؤشر وظيفة
  • تحسينات في C ++ 14

    • نوع الانتاج العائد
    • التقاط مع مهيئ
    • التقاط متغير عضو
    • تعبيرات لامدا العامة

القائمة أعلاه ليست سوى جزء من تاريخ تعبيرات لامدا!

الآن دعونا نرى ما الذي تغير في C ++ 17 وما حصلنا عليه في C ++ 20!

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

قياسي (مسودة قبل النشر) قسم N659 على lambdas: [expr.prim.lambda] . جلبت C ++ 17 تحسينين مهمين لتعبيرات lambda:

  • constexpr امدا
  • التقاط * هذا

ماذا تعني هذه الابتكارات بالنسبة لنا؟ دعونا معرفة ذلك.

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

بدايةً من C ++ 17 ، يعرّف المعيار ضمنيًا operator() لنوع lambda كـ constexpr ، إن أمكن:
من expr.prim.lambda # 4 :
عامل استدعاء دالة هو دالة constexpr إذا كان إعلان المعلمة الشرط للتعبير lambda المقابل متبوعًا بـ constexpr ، أو أنه يفي بمتطلبات وظيفة constexpr.

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

 constexpr auto Square = [] (int n) { return n*n; }; // implicitly constexpr static_assert(Square(2) == 4); 

تذكر أنه في C ++ 17 constexpr يجب أن تتبع الدالة القواعد التالية:

  • لا ينبغي أن يكون الظاهري.

    • يجب أن يكون نوع الإرجاع نوعًا حرفيًا ؛
    • يجب أن يكون كل نوع من أنواع المعلمات نوعًا حرفيًا ؛
    • يجب أن يكون نصها = delete أو = default أو عبارة مركبة لا تحتوي على
      • التعاريف asm
      • تعبيرات غوتو ،
      • تسميات،
      • حاول كتلة أو
      • تعريف المتغير غير الحرفي ، أو المتغير الثابت ، أو متغير الذاكرة المتدفقة الذي لم يتم إجراء التهيئة له.

ماذا عن مثال عملي أكثر؟

 template<typename Range, typename Func, typename T> constexpr T SimpleAccumulate(const Range& range, Func func, T init) { for (auto &&elem: range) { init += func(elem); } return init; } int main() { constexpr std::array arr{ 1, 2, 3 }; static_assert(SimpleAccumulate(arr, [](int i) { return i * i; }, 0) == 14); } 

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

يستخدم الرمز constexpr lambda ، ثم يتم تمريره إلى خوارزمية SimpleAccumulate البسيطة. تستخدم الخوارزمية العديد من عناصر C ++ 17: الآن أصبحت constexpr إضافات constexpr إلى std::array ، std::begin constexpr و std::end (المستخدمة في حلقة مع نطاق) ، وهذا يعني أنه يمكن تنفيذ جميع الشفرات في وقت الترجمة.

بالطبع ، هذا ليس كل شيء.

يمكنك التقاط المتغيرات (شريطة أن تكون constexpr كذلك):

 constexpr int add(int const& t, int const& u) { return t + u; } int main() { constexpr int x = 0; constexpr auto lam = [x](int n) { return add(x, n); }; static_assert(lam(10) == 10); } 

ولكن هناك حالة مثيرة للاهتمام عندما لا تمرر المتغير الذي تم التقاطه إلى أبعد من ذلك ، على سبيل المثال:

 constexpr int x = 0; constexpr auto lam = [x](int n) { return n + x }; 

في هذه الحالة ، في Clang يمكننا الحصول على التحذير التالي:

warning: lambda capture 'x' is not required to be captured for this use

ربما يرجع هذا إلى حقيقة أنه يمكن تغيير x في مكانه مع كل استخدام (ما لم تنقله أكثر أو تأخذ عنوان هذا الاسم).

ولكن من فضلك قل لي إذا كنت تعرف القواعد الرسمية لهذا السلوك. لقد وجدت فقط (من cppreference ) (لكن لا يمكنني العثور عليه في المسودة ...)

(ملاحظة المترجم: أثناء كتابة قرائنا ، ربما أعني استبدال قيمة "x" في كل مكان يتم استخدامه فيه. من المستحيل بالتأكيد تغييره).

يستطيع تعبير lambda قراءة قيمة المتغير دون التقاطه إذا كان المتغير
* يحتوي على عدد صحيح non-volatile أو نوع تعداد ثابت وقد تمت تهيئته باستخدام constexpr أو
* هو constexpr وليس لديه أعضاء قابلة للتغيير.

كن مستعدًا للمستقبل:

في C ++ 20 ، سيكون لدينا خوارزميات قياسية constexpr ، وربما حتى بعض الحاويات ، لذلك constexpr constexpr lambdas مفيدة للغاية في هذا السياق. سيبدو الرمز الخاص بك هو نفسه بالنسبة لإصدار وقت التشغيل بالإضافة إلى إصدار constexpr (إصدار وقت الترجمة)!

باختصار:

تتيح لك constexpr lambda أن تكون متسقًا مع برمجة قواعد البيانات وربما تحتوي على كود أقصر.

الآن دعنا ننتقل إلى الميزة الهامة الثانية المتوفرة في C ++ 17:

القبض على * هذا
التقاط * هذا

هل تتذكر مشكلتنا عندما أردنا القبض على عضو في الفصل؟ بشكل افتراضي ، نلتقط هذا (كمؤشر!) ، وبالتالي قد نواجه مشكلات عند خروج الكائنات المؤقتة عن نطاقها ... يمكن إصلاح ذلك باستخدام طريقة الالتقاط باستخدام مُهيئ (انظر الجزء الأول من السلسلة). لكن الآن ، في C ++ 17 ، لدينا طريقة مختلفة. يمكننا التفاف نسخة من * هذا:

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

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

التقاط المتغير العضو المطلوب باستخدام الالتقاط باستخدام أداة التهيئة يحميك من الأخطاء المحتملة بقيم مؤقتة ، لكن لا يمكننا فعل الشيء نفسه عندما نريد استدعاء طريقة مثل:

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

 struct Baz { auto foo() { return [this] { print(); }; } void print() const { std::cout << s << '\n'; } std::string s; }; 

في C ++ 14 ، الطريقة الوحيدة لجعل التعليمات البرمجية أكثر أمانًا هي التقاط this باستخدام أداة تهيئة:

 auto foo() { return [self=*this] { self.print(); }; }   C ++ 17    : auto foo() { return [*this] { print(); }; } 

شيء آخر:

لاحظ أنه إذا كتبت [=] في وظيفة عضو ، فسيتم التقاط ذلك ضمنيًا! قد يؤدي هذا إلى حدوث أخطاء في المستقبل ... وسيصبح قديمًا في الإصدار C ++ 20.

لذلك نأتي إلى القسم التالي: المستقبل.

المستقبل مع C ++ 20

في C ++ 20 ، نحصل على الوظائف التالية:


في معظم الحالات ، الوظائف التي تم إدخالها حديثًا "توضح" استخدام lambda ، وتسمح ببعض حالات الاستخدام المتقدمة.

على سبيل المثال ، مع P1091 ، يمكنك التقاط رابط منظم.

لدينا أيضًا توضيحات متعلقة بالتقاط هذا. في C ++ 20 ، ستتلقى تحذيرًا إذا قمت بالتقاط [=] بأسلوب:

 struct Baz { auto foo() { return [=] { std::cout << s << std::endl; }; } std::string s; }; GCC 9: warning: implicit capture of 'this' via '[=]' is deprecated in C++20 

إذا كنت بحاجة حقًا إلى التقاط هذا ، فيجب عليك كتابة [=, this] .

هناك أيضًا تغييرات متعلقة بحالات الاستخدام المتقدمة ، مثل سياقات عديمي الجنسية ولامدات عديمي الجنسية التي يمكن إنشاؤها افتراضيًا.

مع كل التغييرات ، يمكنك الكتابة:

 std::map<int, int, decltype([](int x, int y) { return x > y; })> map; 

اقرأ دوافع هذه الميزات في الإصدار الأول من الجمل: P0315R0 و P0624R0 .

ولكن دعونا ننظر إلى ميزة واحدة مثيرة للاهتمام: قوالب لامدا.

نمط لامد

في الإصدار C ++ 14 ، حصلنا على lambdas المعمم ، مما يعني أن المعلمات المعلنة تلقائيًا هي معلمات للقالب.

لامدا:

 [](auto x) { x; } 

ينشئ المحول البرمجي عبارة استدعاء يطابق طريقة boilerplate التالية:

 template<typename T> void operator(T x) { x; } 

ولكن لم يكن هناك طريقة لتغيير معلمة القالب هذه واستخدام وسيطات القالب الفعلية. في C ++ 20 ، سيكون ذلك ممكنًا.

على سبيل المثال ، كيف يمكننا الحد من لامدا لدينا للعمل فقط مع ناقلات من نوع ما؟

يمكننا كتابة لامدا العام:

 auto foo = []<typename T>(const auto& vec) { std::cout<< std::size(vec) << '\n'; std::cout<< vec.capacity() << '\n'; }; 

ولكن إذا كنت أسميها بمعلمة int (على سبيل المثال ، foo(10); ) ، فقد تحصل على بعض الأخطاء التي يصعب قراءتها:

 prog.cc: In instantiation of 'main()::<lambda(const auto:1&)> [with auto:1 = int]': prog.cc:16:11: required from here prog.cc:11:30: error: no matching function for call to 'size(const int&)' 11 | std::cout<< std::size(vec) << '\n'; 

في C ++ 20 يمكننا الكتابة:

 auto foo = []<typename T>(std::vector<T> const& vec) { std::cout<< std::size(vec) << '\n'; std::cout<< vec.capacity() << '\n'; }; 

يسمح lambda أعلاه ببيان مكالمة القالب:

 <typename T> void operator(std::vector<T> const& s) { ... } 

تتبع المعلمة قالب جملة الالتقاط [] .

إذا اتصلت بها بـ int (foo(10);) ، فستتلقى رسالة أجمل:

 note: mismatched types 'const std::vector<T>' and 'int' 


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

في المثال أعلاه ، يمكن للمترجم أن يحذرنا من التناقضات في واجهة lambda عن الكود الموجود داخل الجسم.

جانب مهم آخر هو أنه في lambda العالمي لديك فقط متغير ، وليس نوع القالب. لذلك ، إذا كنت ترغب في الوصول إليه ، يجب عليك استخدام Dectype (x) (لتعبير lambda باستخدام الوسيطة (auto x)). هذا يجعل بعض التعليمات البرمجية مطوّلة ومعقدة أكثر.

على سبيل المثال (باستخدام الكود من P0428):

 auto f = [](auto const& x) { using T = std::decay_t<decltype(x)>; T copy = x; T::static_function(); using Iterator = typename T::iterator; } 

الآن يمكنك الكتابة على النحو التالي:

 auto f = []<typename T>(T const& x) { T::static_function(); T copy = x; using Iterator = typename T::iterator; } 

في القسم أعلاه ، كان لدينا نظرة عامة مختصرة عن C ++ 20 ، لكن لدي حالة استخدام إضافية لك. هذه التقنية ممكنة حتى في C ++ 14. لذلك اقرأ على.

مكافأة - الرفع مع lambdas

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

 // two overloads: void foo(int) {} void foo(float) {} int main() { std::vector<int> vi; std::for_each(vi.begin(), vi.end(), foo); } 

حصلنا على الخطأ التالي من GCC 9 (trunk):

 error: no matching function for call to for_each(std::vector<int>::iterator, std::vector<int>::iterator, <unresolved overloaded function type>) std::for_each(vi.begin(), vi.end(), foo); ^^^^^ 

ومع ذلك ، هناك خدعة حيث يمكننا استخدام لامدا ومن ثم استدعاء وظيفة الحمل الزائد المطلوبة.

في النموذج الأساسي ، بالنسبة لأنواع القيم البسيطة ، بالنسبة إلى وظيفتين ، يمكننا كتابة التعليمات البرمجية التالية:

 std::for_each(vi.begin(), vi.end(), [](auto x) { return foo(x); }); 

وفي الشكل العام ، نحتاج إلى كتابة المزيد:

 #define LIFT(foo) \ [](auto&&... x) \ noexcept(noexcept(foo(std::forward<decltype(x)>(x)...))) \ -> decltype(foo(std::forward<decltype(x)>(x)...)) \ { return foo(std::forward<decltype(x)>(x)...); } 

رمز معقدة جدا ... أليس كذلك؟ :)

دعنا نحاول فك تشفيرها:

نخلق لامدا عام ومن ثم تمرير كل الحجج التي نحصل عليها. لتحديده بشكل صحيح ، نحتاج إلى تحديد noexcept ونوع قيمة الإرجاع. لهذا السبب يتعين علينا تكرار رمز الاتصال - من أجل الحصول على الأنواع الصحيحة.
يعمل ماكرو LIFT في أي برنامج مترجم يدعم C ++ 14.

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

استنتاج

في هذا المنشور ، نظرنا في التغييرات المهمة في C ++ 17 ، وقدمنا ​​نظرة عامة على الميزات الجديدة في C ++ 20.

قد تلاحظ أنه مع كل تكرار للغة ، تختلط تعبيرات lambda بعناصر C ++ أخرى. على سبيل المثال ، قبل C ++ 17 ، لم نتمكن من استخدامها في سياق constexpr ، ولكن الآن أصبح ذلك ممكنًا. بشكل مشابه مع lambdas العامة التي تبدأ بـ C ++ 14 وتطورها إلى C ++ 20 في شكل lambdas للقالب. هل أفتقد شيئًا؟ ربما لديك بعض الأمثلة المثيرة؟ واسمحوا لي أن أعرف في التعليقات!

مراجع

C ++ 11 - [expr.prim.lambda]
C ++ 14 - [expr.prim.lambda]
C ++ 17 - [expr.prim.lambda]
تعبيرات Lambda في C ++ | مستندات مايكروسوفت
Simon Brand - تمرير مجموعات التحميل الزائد إلى وظائف
Jason Turner - C ++ Weekly - Ep 128 - C ++ 20's Template Syntax for Lambdas
جايسون تيرنر - C ++ ويكلي - الحلقة 41 - C ++ 17 في دعم Lambda

ندعو الجميع لحضور ندوة عبر الإنترنت التقليدية المجانية في الدورة ، والتي ستعقد غدا 14 يونيو.

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


All Articles