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