. كانت هناك مشاركة سريعة من جانب العلامة التجارية سيمون الخاصة بنا والتي تتضمن بعض...">

بسّط شفرتك من خلال علم الصواريخ: مشغل C ++ 20's Spaceship

يضيف C ++ 20 مشغلًا جديدًا يطلق عليه اسم المشغل "سفينة الفضاء": <=> . كانت هناك مشاركة سريعة من جانب العلامة التجارية سيمون الخاصة بنا والتي تتضمن بعض المعلومات المتعلقة بهذا المشغل الجديد إلى جانب بعض المعلومات المفاهيمية حول ماهية ما تقوم به وما تقوم به. الهدف من هذا المنشور هو استكشاف بعض التطبيقات الملموسة لهذا المشغل الجديد الغريب والنظير المرتبط به ، operator== (نعم تم تغييره ، للأفضل!) ، كل ذلك مع توفير بعض الإرشادات لاستخدامه في التعليمات البرمجية اليومية.



مقارنات


ليس شيئًا غير شائع أن ترى الشفرة كما يلي:

 struct IntWrapper {  int value;  constexpr IntWrapper(int value): value{value} { }  bool operator==(const IntWrapper& rhs) const { return value == rhs.value; }  bool operator!=(const IntWrapper& rhs) const { return !(*this == rhs);    }  bool operator<(const IntWrapper& rhs)  const { return value < rhs.value;  }  bool operator<=(const IntWrapper& rhs) const { return !(rhs < *this);    }  bool operator>(const IntWrapper& rhs)  const { return rhs < *this;        }  bool operator>=(const IntWrapper& rhs) const { return !(*this < rhs);    } }; 

ملاحظة: سوف يلاحظ القراء النسر أن هذا هو بالفعل أقل مطوّلة مما ينبغي أن يكون في كود ما قبل C ++ 20 لأن هذه الوظائف يجب أن تكون جميعها في الواقع أصدقاء غير أعضاء ، المزيد عن ذلك لاحقًا.

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

 constexpr bool is_lt(const IntWrapper& a, const IntWrapper& b) {  return a < b; } int main() {  static_assert(is_lt(0, 1)); } 

أول شيء ستلاحظه هو أن هذا البرنامج لن يتم تجميعه.

error C3615: constexpr function 'is_lt' cannot result in a constant expression

آه! المشكلة هي أننا نسينا constexpr على وظيفة المقارنة لدينا ، دارات! لذلك يذهب واحد ويضيف constexpr لجميع مشغلي المقارنة. بعد بضعة أيام يذهب شخص ما ويضيف مساعد is_gt لكن يلاحظ أن جميع مشغلي المقارنة ليس لديهم مواصفات استثناء ويمر بنفس العملية الشاقة المتمثلة في إضافة noexcept إلى كل من الأحمال الخمسة الزائدة.

هذا هو المكان الذي يتدخل فيه مشغل سفينة الفضاء C ++ 20 الجديد لمساعدتنا. دعونا نرى كيف يمكن كتابة IntWrapper الأصلي في عالم C ++ 20:

 #include <compare> struct IntWrapper {  int value;  constexpr IntWrapper(int value): value{value} { }  auto operator<=>(const IntWrapper&) const = default; }; 

الفرق الأول الذي قد تلاحظه هو التضمين الجديد لـ <compare> . يكون رأس <compare> مسؤولًا عن ملء برنامج التحويل البرمجي بجميع أنواع فئات المقارنة اللازمة لمشغل سفينة الفضاء لإرجاع نوع مناسب لوظيفة وظيفتنا الافتراضية. في المقتطف أعلاه ، سيتم std::strong_ordering نوع الإرجاع auto على std::strong_ordering .

لم نقم فقط بإزالة 5 خطوط زائدة ، ولكن ليس علينا حتى تحديد أي شيء ، بل إن المترجم يفعل ذلك من أجلنا! تبقى لدينا دون تغيير وتعمل فقط في حين لا تزال constexpr على الرغم من أننا لم نحدد ذلك صراحة في operator<=> constexpr لدينا operator<=> . هذا جيد وجيد لكن بعض الناس قد is_lt رؤوسهم حول سبب is_lt السماح is_lt على الرغم من أنه لا يستخدم مشغل سفينة الفضاء مطلقًا. دعنا نستكشف إجابة هذا السؤال.

إعادة كتابة التعبيرات


في C ++ 20 ، يتم تقديم المترجم لمفهوم جديد يُشار إلى التعبيرات "المعاد كتابتها". يعد مشغل سفينة الفضاء ، إلى جانب operator== ، من بين المرشحين الأولين الخاضعين لتعبيرات معاد كتابتها. للحصول على مثال أكثر واقعية is_lt كتابة التعبير ، دعونا is_lt المثال الوارد في is_lt .

أثناء دقة التحميل الزائد ، سيقوم المحول البرمجي بالاختيار من بين مجموعة من المرشحين القادرين على البقاء ، وكلها تتوافق مع المشغل الذي نبحث عنه. يتم تغيير عملية تجميع المرشحين بشكل طفيف بالنسبة لحالة العمليات الترابطية والمعادلة حيث يجب على المحول البرمجي أيضًا جمع المرشحين المعاد كتابتهم وتوليفهم ( [over.match.oper] /3.4 ).

بالنسبة إلى تعبيرنا a < b يشير a < b إلى المعيار إلى أنه يمكننا البحث في نوع operator<=> أو عن operator<=> نطاق مساحة الاسم operator<=> الذي يقبل نوعه. لذا فإن المترجم يفعل ويجد أنه في الواقع ، IntWrapper::operator<=> النوع IntWrapper::operator<=> . ثم يُسمح للمترجم باستخدام هذا العامل وإعادة كتابة التعبير a < b كـ (a <=> b) < 0 . ثم يتم استخدام تعبير إعادة كتابة هذا كمرشح لقرار التحميل الزائد العادي.

قد تجد نفسك تسأل لماذا هذا التعبير المعاد كتابته صحيح وصحيح. تنبع صحة التعبير بالفعل من الدلالات التي يوفرها مشغل سفينة الفضاء. <=> هي عبارة عن مقارنة ثلاثية حيث تشير إلى أنك لا تحصل على نتيجة ثنائية فحسب ، بل على ترتيب (في معظم الحالات) وإذا كان لديك طلب ، فيمكنك التعبير عن هذا الطلب من حيث أي عمليات علائقية. مثال سريع ، التعبير 4 <=> 5 في C ++ 20 سيعطيك النتيجة std::strong_ordering::less . تعني النتيجة std::strong_ordering::less أن 4 لا يختلف فقط عن 5 ولكنه أقل من تلك القيمة تمامًا ، وهذا يجعل تطبيق العملية (4 <=> 5) < 0 صحيحًا ودقيقًا تمامًا لوصف النتيجة التي توصلنا إليها.

باستخدام المعلومات الموجودة أعلاه ، يمكن للمجمع أن يأخذ أي مشغل علائق معمم (مثل < ، > ، إلخ) وإعادة كتابته وفقًا لمشغل سفينة الفضاء. في المعيار ، غالباً ما يشار إلى التعبير المعاد كتابته بـ (a <=> b) @ 0 حيث تمثل @ أي عملية علائقية.

توليف التعبيرات


ربما لاحظ القراء الإشارة الدقيقة للتعبيرات "المُصنَّعة" أعلاه ويلعبون دورًا في عملية إعادة كتابة المشغل هذه. النظر في وظيفة المسند مختلفة:

 constexpr bool is_gt_42(const IntWrapper& a) {  return 42 < a; } 

إذا استخدمنا تعريفنا الأصلي لبرنامج IntWrapper فلن يتم تجميع هذا الرمز.

error C2677: binary '<': no global operator found which takes type 'const IntWrapper' (or there is no acceptable conversion)

هذا منطقي في أرض ما قبل C ++ 20 ، والطريقة لحل هذه المشكلة تتمثل في إضافة بعض وظائف friend الإضافية إلى IntWrapper والتي تأخذ الجانب الأيسر من int . إذا حاولت إنشاء تلك العينة باستخدام برنامج التحويل البرمجي C ++ 20 وتعريف C ++ 20 الخاص بـ IntWrapper فقد تلاحظ أنه ، مرة أخرى ، "يعمل فقط" - أداة إزالة شعر الرأس. دعنا نفحص لماذا لا يزال مسموحًا ترجمة التعليمات البرمجية أعلاه في C ++ 20.

أثناء تحليل الحمولة الزائدة ، سيقوم المترجم بجمع ما يشير إليه المعيار كمرشحين "توليفيين" ، أو تعبير معاد كتابته بترتيب المعلمات المعكوسة. في المثال أعلاه ، سيحاول المترجم استخدام التعبير المعاد كتابته (42 <=> a) < 0 لكنه سيجد أنه لا يوجد تحويل من IntWrapper إلى int لإرضاء الجانب الأيسر بحيث يتم إسقاط التعبير المعاد كتابته. يستحضر المترجم أيضًا التعبير " IntWrapper " 0 < (a <=> 42) ويجد أن هناك تحويلًا من int إلى IntWrapper خلال مُنشئ التحويل الخاص به بحيث يتم استخدام هذا المرشح.

يتمثل الهدف من التعبيرات المركبة في تجنب فوضى الحاجة إلى كتابة سجل وظائف friend لملء الفجوات حيث يمكن تحويل كائنك من أنواع أخرى. يتم تعبير التعبيرات المركبة إلى 0 @ (b <=> a) .

أنواع أكثر تعقيدا


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

 struct Basics {  int i;  char c;  float f;  double d;  auto operator<=>(const Basics&) const = default; }; struct Arrays {  int ai[1];  char ac[2];  float af[3];  double ad[2][2];  auto operator<=>(const Arrays&) const = default; }; struct Bases : Basics, Arrays {  auto operator<=>(const Bases&) const = default; }; int main() {  constexpr Bases a = { { 0, 'c', 1.f, 1. },                        { { 1 }, { 'a', 'b' }, { 1.f, 2.f, 3.f }, { { 1., 2. }, { 3., 4. } } } };  constexpr Bases b = { { 0, 'c', 1.f, 1. },                        { { 1 }, { 'a', 'b' }, { 1.f, 2.f, 3.f }, { { 1., 2. }, { 3., 4. } } } };  static_assert(a == b);  static_assert(!(a != b));  static_assert(!(a < b));  static_assert(a <= b);  static_assert(!(a > b));  static_assert(a >= b); } 

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

يبدو وكأنه بطة ، يسبح مثل بطة ، و Quacks Like operator==


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

المثال الكنسي هو مقارنة سلسلتين. إذا كان لديك السلسلة "foobar" وقمت بمقارنتها بالسلسلة "foo" باستخدام == يتوقع المرء أن تكون هذه العملية ثابتة تقريبًا. وبالتالي فإن خوارزمية مقارنة السلسلة الفعالة هي:

  • أولاً قم بمقارنة حجم السلسلتين ، إذا كانت الأحجام تختلف عن الإرجاع false ، وإلا
  • تخطي كل عنصر من السلسلتين في انسجام تام وقارن حتى يختلف أحدهما أو يتم الوصول إلى النهاية ، قم بإرجاع النتيجة.

وفقًا لقواعد مشغل سفينة الفضاء ، نحتاج إلى البدء بالمقارنة العميقة لكل عنصر أولاً حتى نجد العنصر المختلف. في مثالنا على "foobar" و "foo" فقط عند مقارنة 'b' مع '\0' هل ترجع أخيرًا إلى false .

لمحاربة هذا كان هناك ورقة ، P1185R2 التي تفاصيل طريقة للمترجم لإعادة كتابة وتوليد operator== بشكل مستقل عن مشغل سفينة الفضاء. يمكن كتابة IntWrapper لدينا على النحو التالي:

 #include <compare> struct IntWrapper {  int value;  constexpr IntWrapper(int value): value{value} { }  auto operator<=>(const IntWrapper&) const = default;  bool operator==(const IntWrapper&) const = default; }; 

خطوة واحدة فقط ... ومع ذلك ، هناك أخبار جيدة ؛ لا تحتاج في الواقع إلى كتابة الكود أعلاه ، لأن مجرد كتابة auto operator<=>(const IntWrapper&) const = default يكفي للمترجم لإنشاء ضمنيًا منفصل وأكثر فاعلية operator== لك!

يطبق المحول البرمجي قاعدة "إعادة كتابة" تم تغييرها قليلاً خاصة بـ == و != حيث تتم إعادة كتابة هذه العوامل في شروط operator== وليس operator<=> . هذا يعني أن != يستفيد أيضًا من التحسين أيضًا.

الرمز القديم لن يكسر


في هذه المرحلة ، ربما تفكر ، حسناً إذا كان المترجم يُسمح له بتنفيذ هذا المشغل لإعادة كتابة ما يحدث عندما أحاول التغلب على المترجم:

 struct IntWrapper {  int value;  constexpr IntWrapper(int value): value{value} { }  auto operator<=>(const IntWrapper&) const = default;  bool operator<(const IntWrapper& rhs) const { return value < rhs.value; } }; constexpr bool is_lt(const IntWrapper& a, const IntWrapper& b) {  return a < b; } 

الجواب هنا هو أنك لم تفعل. يحتوي نموذج دقة التحميل الزائد في C ++ على هذه الساحة حيث يخوض جميع المرشحين معركة ، وفي هذه المعركة المحددة ، لدينا 3 مرشحين:

  • IntWrapper::operator<(const IntWrapper& a, const IntWrapper& b)
  • IntWrapper::operator<=>(const IntWrapper& a, const IntWrapper& b)

(إعادة)

  • IntWrapper::operator<=>(const IntWrapper& b, const IntWrapper& a)

(تجميعي)

إذا قبلنا قواعد دقة التحميل الزائد في C ++ 17 ، فستكون نتيجة هذه المكالمة غامضة ، ولكن تم تغيير قواعد دقة التحميل الزائد C ++ 20 للسماح للمترجم لحل هذا الموقف إلى التحميل الزائد المنطقي.

هناك مرحلة من دقة التحميل الزائد حيث يجب على المحول البرمجي إجراء سلسلة ربط كاسحة. في C ++ 20 ، هناك رابط كسر جديد ينص على أنه يجب علينا أن نفضل الأحمال الزائدة التي لم تتم إعادة كتابتها أو توليفها ، وهذا يجعل IntWrapper::operator< overload الخاص IntWrapper::operator< أفضل مرشح ويحل الغموض. هذه الآلية نفسها تمنع المرشحين المركبين من الدوس على التعبيرات المعتادة المعاد كتابتها.

إغلاق الأفكار


مشغل سفينة الفضاء هو إضافة مرحب بها إلى C ++ وهي واحدة من الميزات التي سوف تبسط وتساعدك على كتابة كود أقل ، وفي بعض الأحيان ، يكون الرقم أقل. ربط حزام الأمان حتى مع مشغل سفينة الفضاء C ++ 20!

نحثك على الخروج ومحاولة مشغل سفينة الفضاء ، وهو متاح الآن في Visual Studio 2019 تحت /std:c++latest ! كملاحظة ، ستكون التغييرات التي تم تقديمها من خلال P1185R2 متاحة في الإصدار 16.2 من Visual Studio 2019. يرجى الانتباه إلى أن مشغل سفينة الفضاء هو جزء من C ++ 20 ويخضع لبعض التغييرات حتى وقت الانتهاء من C ++ 20.

كما هو الحال دائمًا ، نرحب بتعليقاتك. لا تتردد في إرسال أي تعليقات عبر البريد الإلكتروني على visualcpp@microsoft.com أو عبر Twittervisualc أو Facebook على Microsoft Visual Cpp . أيضًا ، لا تتردد في متابعتني على Twitterstarfreakclone .

إذا واجهت مشاكل أخرى مع MSVC في VS 2019 ، فيرجى إخبارنا من خلال خيار الإبلاغ عن مشكلة ، إما من المثبت أو من Visual Studio IDE نفسه. للحصول على اقتراحات أو تقارير الأخطاء ، أخبرنا من خلال DevComm.

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


All Articles