الأماكن الزلقة في C ++ 17

صورة

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

اليوم سأخبرك لماذا إذا لم يكن constexpr بديلاً لوحدات الماكرو ، فما هي "الأجزاء الداخلية" للربط المنظم و "المزالق" وهل صحيح أن نسخة elision تعمل دائمًا الآن ويمكنك كتابة أي عائد دون تردد.

إذا لم تكن خائفًا من جعل يديك متسخة قليلاً ، والتخبط في "الدواخل" لسانك ، مرحبًا بك في Cat.



إذا constexpr


لنبدأ if constexpr - if constexpr لك if constexpr فرع التعبير الشرطي الذي لم يتم الوفاء بالشرط المرغوب فيه حتى في مرحلة التجميع.

يبدو أن هذا استبدال الماكرو #if لإيقاف تشغيل المنطق "إضافية"؟ لا. لا على الإطلاق.

أولاً ، يحتوي if على خصائص غير متوفرة لوحدات الماكرو - بداخله يمكنك حساب أي تعبير constexpr يمكن constexpr إلى bool . حسنًا ، وثانياً ، يجب أن تكون محتويات الفرع المهملة صحيحة بناءً على دلالات.

نظرًا للشرط الثاني ، if constexpr استخدام if constexpr ، على سبيل المثال ، لا يمكن فصل الوظائف غير الموجودة (لا يمكن فصل الكود المعتمد على النظام الأساسي بشكل صريح بهذه الطريقة) أو سيئ من وجهة نظر لغة الإنشاء (على سبيل المثال ، " void T = 0; ").

ما هي if constexpr من استخدام if constexpr ؟ النقطة الرئيسية هي في القوالب. هناك قاعدة خاصة بالنسبة لهم: لا يتم إنشاء مثيل للفرع المهملة عند إنشاء مثيل للقالب. هذا يجعل من السهل كتابة التعليمات البرمجية التي تعتمد بطريقة ما على خصائص أنواع القوالب.

ومع ذلك ، في القوالب ، يجب ألا ينسى المرء أن الكود الموجود داخل الفروع يجب أن يكون صحيحًا على الأقل لبعض المتغيرات (ربما حتى المحض) المتغيرة من إنشاء مثيل ، لذلك من static_assert(false) ببساطة الكتابة ، على سبيل المثال ، static_assert(false) داخل أحد الفروع (من الضروري أن يكون هذا يعتمد static_assert على بعض المعلمات التابعة للقالب).

الأمثلة على ذلك:

 void foo() {    //    ,       if constexpr ( os == OS::win ) {        win_api_call(); //         }    else {        some_other_os_call(); //  win      } } 

 template<class T> void foo() {    //    ,    T      if constexpr ( os == OS::win ) {        T::win_api_call(); //  T   ,    win    }    else {        T::some_other_os_call(); //  T   ,         } } 

 template<class T> void foo() {    if constexpr (condition1) {        // ...    }    else if constexpr (condition2) {        // ...    }    else {        // static_assert(false); //          static_assert(trait<T>::value); // ,   ,  trait<T>::value   false    } } 

أشياء يجب تذكرها


  1. يجب أن يكون الرمز في جميع الفروع صحيحًا.
  2. داخل القوالب ، لا يتم إنشاء محتويات الفروع المهملة.
  3. يجب أن يكون الكود الموجود داخل أي فرع صحيحًا لمتغير محتمل واحد على الأقل من إنشاء مثيل للقالب.

ملزمة منظم




في C ++ 17 ، ظهرت آلية ملائمة إلى حد ما لتحليل العديد من الكائنات المشابهة للقطع ، مما يسمح لك بربط عناصرها الداخلية بشكل مريح ودقيق مع المتغيرات المسماة:

 //     —    : for (const auto& [key, value] : map) {    std::cout << key << ": " << value << std::endl; } 

بواسطة كائن يشبه tuple ، أعني مثل هذا الكائن الذي يعرف به عدد العناصر الداخلية المتاحة في وقت الترجمة (من "tuple" - قائمة مرتبة مع عدد محدد من العناصر (متجه)).

يتضمن هذا التعريف أنواعًا مثل: std::pair ، std::tuple ، std::array ، صفائف النموذج " T a[N] " ، بالإضافة إلى مختلف الهياكل والفئات المكتوبة ذاتيا.

توقف ... يمكنك استخدام الهياكل الخاصة بك في الربط الهيكلي؟ المفسد: يمكنك (رغم أنك في بعض الأحيان تضطر إلى العمل بجد (ولكن المزيد في هذا أدناه)).

كيف يعمل؟


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

يوفر المعيار بناء الجملة التالي لتحديد الربط:

att (اختياري) cv-auto ref-operator (اختياري) [ معرف قائمة ] التعبير ؛

  • attr - قائمة السمات الاختيارية ؛
  • cv-auto - السيارات مع المعدلات const / متقلبة ممكن.
  • ref-operator - محدد المرجع المرجعي (& و &&) ؛
  • identifier-list - قائمة بأسماء المتغيرات الجديدة ؛
  • expression هو تعبير ينتج عنه كائن يشبه tuple يُستخدم للربط (يمكن أن يكون التعبير في النموذج " = expr " أو " {expr} " أو " (expr) ").

من المهم ملاحظة أن عدد الأسماء في identifier-list يجب أن يتطابق مع عدد العناصر في الكائن الناتج عن expression .

كل هذا يسمح لك بكتابة إنشاءات النموذج:

 const volatile auto && [a,b,c] = Foo{}; 

وهنا نصل إلى المكان الأول "الزلق": تلبية تعبير عن النموذج " auto a = expr; "، تقصد عادة أن النوع" a "سيتم حسابه بالتعبير" expr "، وتتوقع أنه في التعبير" const auto& [a,b,c] = expr; "سيتم تنفيذ نفس الشيء ، فقط أنواع" a,b,c "هي أنواع const& المقابلة لـ" expr "...

الحقيقة مختلفة: يستخدم محدد cv-auto ref-operator لحساب نوع المتغير غير المرئي ، حيث يتم تعيين نتيجة حساب expr (أي ، يستبدل المترجم " const auto& [a,b,c] = expr " بـ " const auto& e = expr ").

وبالتالي ، يظهر كيان غير مرئي جديد (يشار إليه فيما يلي {e}) ، ومع ذلك ، فإن الكيان مفيد للغاية: على سبيل المثال ، يمكن أن يتحقق كائنات مؤقتة (وبالتالي ، يمكنك توصيلها بأمان " const auto& [a,b,c] = Foo {}; ").

يتبع المكان الزلق الثاني مباشرة من الاستبدال الذي يقوم به المترجم: إذا كان النوع المستخلص من {e} ليس مرجعًا ، فسيتم نسخ نتيجة expr إلى {e}.

ما هي أنواع المتغيرات في identifier-list ؟ بادئ ذي بدء ، لن تكون هذه متغيرات بالضبط. نعم ، إنها تتصرف مثل المتغيرات الحقيقية والعادية ، ولكن فقط مع الفارق الذي يشيرون داخله إلى كيان مرتبط بهم ، وسوف ينتج عنهم decltype من هذا المتغير "المرجع" نوع الكيان الذي يشير إليه هذا المتغير:

 std::tuple<int, float> t(1, 2.f); auto& [a, b] = t; // decltype(a) — int, decltype(b) — float ++a; // ,  « »,   t std::cout << std::get<0>(t); //  2 

يتم تعريف الأنواع نفسها على النحو التالي:

  1. إذا كانت {e} عبارة عن صفيف ( T a[N] ) ، فسيكون النوع واحدًا - T ، وستتزامن المعدلات cv مع الصفيف.
  2. إذا كان {e} من النوع E ويدعم واجهة tuple ، فسيتم تعريف الهياكل:

     std::tuple_size<E> 

     std::tuple_element<i, E> 

    وظيفة:

     get<i>({e}); //  {e}.get<i>() 

    ثم سيكون نوع كل متغير هو النوع std::tuple_element_t<i, E>
  3. في حالات أخرى ، سيتوافق نوع المتغير مع نوع عنصر البنية الذي يتم تنفيذ الربط به.

لذلك ، إذا كانت بإيجاز شديد ، يتم اتخاذ الخطوات التالية مع الارتباط الهيكلي:

  1. حساب نوع وتهيئة الكيان غير المرئي {e} بناءً على معدّلات expr و cv-ref .
  2. أنشئ متغيرات زائفة واربطها بالعناصر {e}.

ربط هيكليا الفصول / الهياكل


العقبة الرئيسية أمام ربط بنياتها هي عدم وجود انعكاس في C ++. حتى المترجم ، الذي ، على ما يبدو ، يجب أن يعرف على وجه اليقين كيف يتم ترتيب هذا الهيكل أو ذاك في الداخل ، ويواجه وقتًا عصيبًا: معدلات الوصول (العامة / الخاصة / المحمية) والميراث يعقد الأمور إلى حد كبير.

بسبب هذه الصعوبات ، فإن القيود المفروضة على استخدام فصولهم صارمة للغاية (على الأقل في الوقت الحالي: P1061 ، P1096 ):

  1. يجب أن تكون جميع الحقول الداخلية غير الثابتة للفصل الدراسي من نفس الفئة الأساسية ، ويجب أن تكون متاحة في وقت الاستخدام.
  2. أو يجب على الفصل تطبيق "الانعكاس" (دعم واجهة tuple).

 //  «»  struct A { int a; }; struct B : A {}; struct C : A { int c; }; class D { int d; }; auto [a] = A{}; //  (a -> A::a) auto [a] = B{}; //  (a -> B::A::a) auto [a, c] = C{}; // : a  c    auto [d] = D{}; // : d — private void D::foo() {    auto [d] = *this; //  (d   ) } 

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

 //  ,      int   class Foo; template<> struct std::tuple_size<Foo> : std::integral_constant<std::size_t, 1> {}; template<> struct std::tuple_element<0, Foo> { using type = int&; }; class Foo { public: template<std::size_t i> std::tuple_element_t<i, Foo> const& get() const; template<std::size_t i> std::tuple_element_t<i, Foo> & get(); private: int _foo = 0; int& _bar = _foo; }; template<> std::tuple_element_t<0, Foo> const& Foo::get<0>() const { return _bar; } template<> std::tuple_element_t<0, Foo> & Foo::get<0>() { return _bar; } 

الآن نحن نربط:

 Foo foo; const auto& [f1] = foo; const auto [f2] = foo; auto& [f3] = foo; auto [f4] = foo; 

وحان الوقت للتفكير في الأنواع التي حصلنا عليها؟ (كل من يستطيع الإجابة على الفور يستحق حلوة لذيذة.)

 decltype(f1); decltype(f2); decltype(f3); decltype(f4); 

الإجابة الصحيحة
 decltype(f1); // int& decltype(f2); // int& decltype(f3); // int& decltype(f4); // int& ++f1; //     foo._foo,  {e}    const 


لماذا حدث هذا؟ تكمن الإجابة في التخصص الافتراضي لـ std::tuple_element :

 template<std::size_t i, class T> struct std::tuple_element<i, const T> { using type = std::add_const_t<std::tuple_element_t<i, T>>; }; 

std::add_const لا تضيف const إلى أنواع المراجع ، لذلك سيكون نوع Foo دائمًا int& .

كيف تربح هذا؟ فقط أضف تخصصًا لـ const Foo :

 template<> struct std::tuple_element<0, const Foo> { using type = const int&; }; 

ثم كل الأنواع ستكون متوقعة:

 decltype(f1); // const int& decltype(f2); // const int& decltype(f3); // int& decltype(f4); // int& ++f1; //     

بالمناسبة ، يكون السلوك نفسه صحيحًا ، على سبيل المثال ، std::tuple<T&>
- يمكنك الحصول على مرجع غير ثابت للعنصر الداخلي ، على الرغم من أن الكائن نفسه سيكون ثابتًا.

أشياء يجب تذكرها


  1. يشير " cv-auto ref [a1..an] = expr " في " cv-auto ref [a1..an] = expr " إلى المتغير غير المرئي {e}.
  2. إذا لم تتم الإشارة إلى النوع المستنتج {e} ، فسيتم تهيئة {e} عن طريق النسخ (بعناية مع فئات "الوزن الثقيل").
  3. متغيرات الحدود هي روابط "ضمنية" (تتصرف مثل الارتباطات ، على الرغم من أن decltype بإرجاع نوع غير مرجعي لها (ما لم يشير المتغير إلى رابط)).
  4. يجب توخي الحذر عند استخدام أنواع المراجع للربط.

إرجاع قيمة التحسين (rvo ، نسخة elision)




ربما كانت هذه واحدة من أكثر الميزات التي تمت مناقشتها بحرارة في معيار C ++ 17 (على الأقل في دائرة أصدقائي). وبالفعل: C ++ 11 جلبت دلالات الحركة ، والتي سهلت إلى حد كبير نقل "الداخلية" للكائن وإنشاء المصانع المختلفة ، و C ++ 17 بشكل عام ، على ما يبدو ، جعلت من الممكن عدم التفكير في كيفية إرجاع الكائن من بعض طريقة المصنع ، - الآن يجب أن يكون كل شيء دون نسخ وبصفة عامة ، "قريباً كل شيء سوف يزهر على المريخ" ...

ولكن لنكن واقعيين إلى حد ما: تحسين القيمة المرجعة ليس أسهل ما يمكن تنفيذه. أوصي بشدة بمشاهدة هذا العرض التقديمي من cppcon2018: Arthur O'Dwyer " إرجاع قيمة التحسين: أصعب مما يبدو " ، حيث يوضح المؤلف سبب صعوبة ذلك.

المفسد القصير:

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

ما يلي من هذا؟ لنفصلها مع الأمثلة.

كل شيء سيكون على ما يرام هنا - سوف يعمل NRVO ، وسيتم بناء الكائن على الفور في "فتحة":

 Base foo1() { Base a; return a; } 

هنا لم يعد من الممكن تحديد أي كائن يجب أن يكون النتيجة بشكل لا لبس فيه ، لذلك سيتم استدعاء مُنشئ الخطوة (c ++ 11) ضمنيًا :

 Base foo2(bool c) { Base a,b; if (c) { return a; } return b; } 

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

 Base foo3(bool c) { Derived a,b; if (c) { return std::move(a); } return std::move(b); } 

يبدو أن هذا هو نفس foo2 ، ولكن المشغل الثلاثي هو شيء غريب للغاية ...

 Base foo4(bool c) { Base a, b; return std::move(c ? a : b); } 

على غرار foo4 ، ولكن أيضًا مع نوع مختلف ، لذلك يلزم move بالضبط:

 Base foo5(bool c) { Derived a, b; return std::move(c ? a : b); } 

كما ترون من الأمثلة ، لا يزال يتعين على المرء أن يفكر في كيفية إرجاع المعنى حتى في الحالات التي تبدو تافهة ... هل هناك أي طرق لتبسيط حياتك قليلاً؟ هناك: clang لبعض الوقت الآن يدعم تشخيص الحاجة إلى استدعاء move بشكل صريح ، وهناك العديد من المقترحات ( P1155 ، P0527 ) في المعيار الجديد الذي سيجعل move الصريح أقل ضرورة.

أشياء يجب تذكرها


  1. RVO / NRVO ستعمل فقط إذا:
    • من المعروف بشكل لا لبس فيه أي كائن مفرد يجب إنشاؤه في "فتحة قيمة الإرجاع" ؛
    • كائن العودة وأنواع الوظيفة هي نفسها.
  2. إذا كان هناك غموض في قيمة الإرجاع ، فعندئذٍ:
    • إذا كانت أنواع الكائن الذي تم إرجاعه وتطابق الوظيفة ، سيتم استدعاء النقل ضمنيًا ؛
    • خلاف ذلك ، يجب عليك استدعاء صراحة التحرك.
  3. توخى الحذر عند المشغل الثلاثي: إنه موجز ولكنه قد يتطلب تحركًا واضحًا.
  4. من الأفضل استخدام المجمعين الذين لديهم تشخيصات مفيدة (أو على الأقل محللات ثابتة).

استنتاج


وحتى الآن أنا أحب C ++ ؛)

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


All Articles