مرحبًا يا هابروفسك. فيما يتعلق ببدء التوظيف في مجموعة جديدة في الدورة التدريبية
"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 ، لدينا طريقة مختلفة. يمكننا التفاف نسخة من * هذا:
يمكنك اللعب باستخدام الكود هنا:
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); });
وفي الشكل العام ، نحتاج إلى كتابة المزيد:
رمز معقدة جدا ... أليس كذلك؟ :)
دعنا نحاول فك تشفيرها:
نخلق لامدا عام ومن ثم تمرير كل الحجج التي نحصل عليها. لتحديده بشكل صحيح ، نحتاج إلى تحديد 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 يونيو.