C ++ 17 में फिसलन वाले स्थान

छवि

हाल के वर्षों में, C ++ लीप्स और सीमा से आगे बढ़ रहा है, और भाषा की सभी सूक्ष्मताओं और जटिलताओं को ध्यान में रखते हुए बहुत मुश्किल हो सकता है। एक नया मानक दूर नहीं है, हालांकि, नए रुझानों की शुरूआत सबसे तेज और सबसे आसान प्रक्रिया नहीं है, इसलिए, जबकि C ++ 20 से पहले थोड़ा समय है, मैं वर्तमान मानक के कुछ विशेष रूप से "फिसलन" स्थानों को ताज़ा करने या खोजने का सुझाव देता हूं। भाषा।

आज मैं आपको बताऊंगा कि अगर कॉन्स्ट्रेक्स मैक्रोज़ के लिए रिप्लेसमेंट नहीं है, तो स्ट्रक्चर्ड बाइंडिंग और उसके "नुकसान" के "इंटर्नल" क्या हैं, और क्या यह सच है कि कॉपी एलिसन अब हमेशा काम करता है और आप बिना किसी हिचकिचाहट के कोई भी रिटर्न लिख सकते हैं।

यदि आप अपने हाथों को थोड़ा गंदा करने से नहीं डरते हैं, तो अपनी जीभ के "इनसाइट्स" में बहक कर, कैट में आपका स्वागत है।



अगर बाधा


आइए सबसे सरल एक से शुरू करें - if constexpr आपको सशर्त अभिव्यक्ति शाखा को छोड़ने की अनुमति देता है जिसके लिए संकलन चरण पर भी वांछित स्थिति पूरी नहीं होती है।

ऐसा लगता है कि यह "अतिरिक्त" तर्क को बंद करने के लिए #if मैक्रो के लिए एक प्रतिस्थापन है? नहीं। बिलकुल नहीं।

सबसे पहले, इस तरह के एक में गुण होते हैं जो मैक्रोज़ के लिए उपलब्ध नहीं हैं - अंदर आप किसी भी constexpr अभिव्यक्ति को गिन सकते हैं constexpr bool constexpr किया जा सकता है। ठीक है, और दूसरी बात, त्याग की गई शाखा की सामग्री को सिंटैक्टिक और शब्दार्थ रूप से सही होना चाहिए।

दूसरी आवश्यकता के कारण, if constexpr उपयोग नहीं किया जा सकता है, उदाहरण के लिए, गैर-मौजूद फ़ंक्शन (प्लेटफ़ॉर्म-निर्भर कोड को इस तरह से स्पष्ट रूप से अलग नहीं किया जा सकता है) या निर्माण भाषा के दृष्टिकोण से खराब है (उदाहरण के लिए, " void T = 0; ")।

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; } 

टपल-जैसी वस्तु से मेरा अभिप्राय ऐसी वस्तु से होगा, जिसके लिए संकलन के समय उपलब्ध आंतरिक तत्वों की संख्या ज्ञात हो ("टपल" से - तत्वों की निश्चित संख्या (वेक्टर) के साथ एक आदेशित सूची)।

इस तरह की परिभाषाएं इस परिभाषा के अंतर्गत आती हैं: std::pair , std::tuple , std::array , रूप की सरणियाँ " T a[N] ", साथ ही विभिन्न स्व-लिखित संरचनाएं और कक्षाएं।

बंद करो ... क्या आप संरचनात्मक बंधन में अपनी खुद की संरचनाओं का उपयोग कर सकते हैं? Spoiler: आप कर सकते हैं (हालांकि कभी-कभी आपको कड़ी मेहनत करनी होती है (लेकिन उस पर और अधिक))।

यह कैसे काम करता है


स्ट्रक्चरल लिंकिंग का काम एक अलग लेख का हकदार है, लेकिन जब से हम विशेष रूप से "फिसलन" स्थानों के बारे में बात कर रहे हैं, मैं संक्षेप में समझाने की कोशिश करूंगा कि सब कुछ कैसे काम करता है।

मानक बाध्यकारी को परिभाषित करने के लिए निम्नलिखित सिंटैक्स प्रदान करता है:

attr (वैकल्पिक) cv- ऑटो रेफरी-ऑपरेटर (वैकल्पिक) [ पहचानकर्ता-सूची ] अभिव्यक्ति ;

  • attr - वैकल्पिक विशेषता सूची;
  • cv-auto - संभावित कास्ट / वाष्पशील संशोधक के साथ ऑटो;
  • ref-operator - वैकल्पिक संदर्भ निर्दिष्टकर्ता (और या &&);
  • identifier-list - नए चर के नामों की एक सूची;
  • expression एक अभिव्यक्ति है जिसके परिणामस्वरूप एक टपल-जैसी वस्तु होती है जिसका उपयोग बंधन के लिए किया जाता है (अभिव्यक्ति " = 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 "के लिए" expr "के संगत प्रकार const& तत्व प्रकार होंगे ...

सच्चाई अलग है: cv-auto ref-operator स्पेसियर का उपयोग एक अदृश्य चर के प्रकार की गणना करने के लिए किया जाता है, जिसमें expr की गणना का परिणाम असाइन किया गया है (अर्थात, कंपाइलर “ const auto& [a,b,c] = expr ” “ const auto& e = expr ” के साथ बदलता है const auto& e = expr ”) से हुआ।

इस प्रकार, एक नई अदृश्य इकाई प्रकट होती है (इसके बाद मैं इसे {e} कहूंगा), हालांकि, इकाई बहुत उपयोगी है: उदाहरण के लिए, यह अस्थायी वस्तुओं को उत्प्रेरित कर सकती है (इसलिए, आप उन्हें सुरक्षित रूप से " const auto& [a,b,c] = Foo {}; से कनेक्ट कर सकते हैं 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-modifiers उन सभी के साथ मेल खाएंगे।
  2. यदि {e} टाइप E का है और टपल इंटरफेस का समर्थन करता है, तो संरचनाएं परिभाषित की गई हैं:

     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. या वर्ग को "प्रतिबिंब" (टपल इंटरफ़ेस का समर्थन) लागू करना चाहिए।

 //  «»  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   ) } 

ट्यूपल इंटरफ़ेस का कार्यान्वयन आपको बाध्यकारी के लिए अपनी किसी भी कक्षा का उपयोग करने की अनुमति देता है, लेकिन यह थोड़ा बोझिल लग रहा है और इसके नुकसान को कम करता है। आइए तुरंत एक उदाहरण का उपयोग करें:

 //  ,      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 नहीं है, इसलिए 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 " में " cv-auto ref [a1..an] = expr " अदृश्य चर {{} को दर्शाता है।
  2. यदि अनुमानित प्रकार {e} को संदर्भित नहीं किया जाता है, तो {e} को कॉपी करके ("हैवीवेट" कक्षाओं के साथ सावधानीपूर्वक) आरंभ किया जाएगा।
  3. बाध्य चर "अंतर्निहित" लिंक हैं (वे लिंक की तरह व्यवहार करते हैं, हालांकि decltype उनके लिए एक गैर-संदर्भ प्रकार देता है (जब तक कि चर एक लिंक को संदर्भित नहीं करता है)।
  4. बाध्यकारी के लिए संदर्भ प्रकारों का उपयोग करते समय देखभाल की जानी चाहिए।

रिटर्न वैल्यू ऑप्टिमाइजेशन (rvo, कॉपी एलीशन)




शायद यह C ++ 17 मानक (कम से कम मेरे दोस्तों के सर्कल में) की सबसे अधिक चर्चित सुविधाओं में से एक था। और वास्तव में: C ++ 11 ने आंदोलन के शब्दार्थों को लाया, जिसने वस्तु के "आंतरिक" के हस्तांतरण और विभिन्न कारखानों के निर्माण को बहुत सरल बना दिया, और सामान्य रूप से C ++ 17, यह प्रतीत होता है, किसी भी कारखाने विधि से ऑब्जेक्ट को कैसे वापस करना है, इसके बारे में सोचना संभव नहीं है। , - अब सब कुछ नकल के बिना होना चाहिए और सामान्य तौर पर, "जल्द ही सब कुछ मंगल पर खिल जाएगा" ...

लेकिन आइए थोड़ा यथार्थवादी बनें: रिटर्न वैल्यू का अनुकूलन लागू करना सबसे आसान काम नहीं है। मैं अत्यधिक cppcon2018 की इस प्रस्तुति को देखने की सलाह देता हूं: आर्थर ओ'डायर " रिटर्न वैल्यू ऑप्टिमाइजेशन: हार्ड थैन इट लुक ", जिसमें लेखक बताता है कि यह क्यों मुश्किल है।

शॉर्ट स्पॉइलर:

"वापसी मूल्य के लिए स्लॉट" जैसी कोई चीज है। यह स्लॉट अनिवार्य रूप से स्टैक पर एक जगह है जिसे कॉल करने वाले और पास करने वाले द्वारा आवंटित किया जाता है। यदि कॉल कोड वास्तव में जानता है कि किस एकल ऑब्जेक्ट को वापस किया जाएगा, तो वह सीधे इस स्लॉट में सीधे बना सकता है (बशर्ते कि ऑब्जेक्ट का आकार और प्रकार और स्लॉट समान हो)।

इससे क्या होता है? आइए इसे उदाहरणों के साथ अलग करते हैं।

यहां सब कुछ ठीक होगा - NRVO काम करेगा, वस्तु का निर्माण तुरंत "स्लॉट" में किया जाएगा:

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

यहां यह स्पष्ट रूप से निर्धारित करना संभव नहीं है कि किस वस्तु का परिणाम होना चाहिए, इसलिए चाल निर्माणकर्ता (सी ++ 11) को संक्षेप में कहा जाएगा :

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

यहाँ यह थोड़ा और अधिक जटिल है ... चूंकि वापसी मूल्य का प्रकार घोषित प्रकार से अलग है, आप स्पष्ट रूप से move नहीं उठा सकते 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); } 

जैसा कि आप उदाहरणों से देख सकते हैं, एक को अभी भी सोचना है कि प्रतीत होता है कि तुच्छ मामलों में भी अर्थ कैसे लौटाया जाए ... क्या आपके जीवन को थोड़ा सरल बनाने के लिए कोई उपाय हैं? ऐसे हैं: कुछ समय के लिए क्लैंग अब स्पष्ट रूप से कॉल move की आवश्यकता के निदान का समर्थन करता है, और नए मानक में कई प्रस्ताव ( P1155 , P0527 ) हैं जो स्पष्ट move कम आवश्यक बना देंगे।

याद रखने योग्य बातें


  1. RVO / NRVO केवल तभी काम करेगा जब:
    • यह स्पष्ट रूप से ज्ञात है कि "वापसी मान स्लॉट" में कौन सी एकल वस्तु बनाई जानी चाहिए;
    • रिटर्न ऑब्जेक्ट और फ़ंक्शन प्रकार समान हैं।
  2. यदि रिटर्न वैल्यू में अस्पष्टता है, तो:
    • यदि लौटे हुए ऑब्जेक्ट और फ़ंक्शन के प्रकार मेल खाते हैं, तो इस कदम को अंतर्निहित रूप से कहा जाएगा;
    • अन्यथा, आपको स्पष्ट रूप से मूव कॉल करना चाहिए।
  3. टर्नरी ऑपरेटर के साथ सावधानी: यह संक्षिप्त है, लेकिन एक स्पष्ट कदम की आवश्यकता हो सकती है।
  4. उपयोगी डायग्नोस्टिक्स (या कम से कम स्थिर विश्लेषक) के साथ संकलक का उपयोग करना बेहतर है।

निष्कर्ष


और फिर भी मैं सी ++ से प्यार करता हूं;)

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


All Articles