. منذ وقت ليس ببعيد ، نشرت Simon Brand منشورًا يحتوي على معلومات مفاهيمية مفصلة حول ماهية هذا المش...">

مشغل سفينة الفضاء الجديد في C ++ 20

يضيف C ++ 20 مشغلًا جديدًا يسمى "سفينة الفضاء": <=> . منذ وقت ليس ببعيد ، نشرت Simon Brand منشورًا يحتوي على معلومات مفاهيمية مفصلة حول ماهية هذا المشغل ولأي أغراض يتم استخدامه. تتمثل المهمة الرئيسية لهذا المنشور في دراسة التطبيقات المحددة للمشغل الجديد "الغريب" والمشغل التماثلي الخاص به 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 إلى كل من 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> مسؤولًا عن ملء المحول البرمجي بجميع أنواع فئات المقارنة اللازمة لمشغل سفينة الفضاء ، بحيث يُرجع نوعًا مناسبًا لوظيفة وظيفتنا الافتراضية. في المقتطف أعلاه ، سيكون نوع الإرجاع auto std::strong_ordering .

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

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


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

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

بالنسبة إلى تعبيرنا a < b ينص المعيار على أنه يمكننا البحث عن النوع a operator<=> أو وظائف 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 . إذا حاولت إنشاء هذا المثال باستخدام برنامج التحويل البرمجي IntWrapper C ++ 20 ، فقد تلاحظ أنه يعمل مرة أخرى. دعونا نلقي نظرة على سبب استمرار تجميع الشفرة أعلاه في 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); } 

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

يشبه البطة ، يسبح مثل البطة ، والدجالين مثل 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== بدلاً من 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 ++ هو الساحة التي يتصادم فيها جميع المرشحين. في هذه المعركة بالذات ، لدينا ثلاثة منهم:

  • 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< بنا 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 .

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

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


All Articles