[CppCon 2018] हर्ब सटर: टुवर्ड्स ए सिंपल एंड मोर पावरफुल C ++


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


प्राक्कथन: अधिक = आसान !?


सी ++ के आरोपों को सुना जाता है कि मानक संवेदनापूर्ण और निर्दयता से बढ़ रहा है। लेकिन यहां तक ​​कि सबसे प्रबल रूढ़िवादियों का तर्क नहीं होगा कि रेंज-फॉर (संग्रह चक्र) और ऑटो (कम से कम चलने वाले के लिए) जैसे नए निर्माण कोड को सरल बनाते हैं। आप अनुमानित मानदंड विकसित कर सकते हैं कि (कम से कम एक, आदर्श रूप से सभी) नई भाषा एक्सटेंशन को अभ्यास में कोड को सरल बनाने के लिए संतुष्ट होना चाहिए:


  1. कोड को कम करें, डुप्लिकेट कोड हटाएं (रेंज-फॉर, ऑटो, लैम्ब्डा, मेटाक्लासेस)
  2. सुरक्षित कोड लिखना आसान करें, त्रुटियों और विशेष मामलों को रोकें (स्मार्ट पॉइंटर्स, लाइफटाइम)
  3. पूरी तरह से पुराने, कम कार्यात्मक सुविधाओं (टाइप्डिफ का उपयोग करके) की जगह

हर्ब सटर "आधुनिक सी ++" की पहचान करता है - आधुनिक कोडिंग मानकों (जैसे सी ++ कोर दिशानिर्देश ) का अनुपालन करने वाली सुविधाओं का एक सबसेट, और पूर्ण मानक को "संगतता मोड" के रूप में मानता है, जिसे सभी को जानने की आवश्यकता नहीं है। तदनुसार, यदि "आधुनिक सी ++" नहीं बढ़ता है, तो सब कुछ ठीक है।


चर (आजीवन) के जीवनकाल की जाँच


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


सत्यापन के बुनियादी सिद्धांत


जीवन समय विश्लेषण के दृष्टिकोण से, प्रकार 3 श्रेणियों में विभाजित हैं:


  • मूल्य वह है जो एक सूचक इंगित कर सकता है।
  • सूचक - मूल्य को संदर्भित करता है, लेकिन अपने जीवनकाल को नियंत्रित नहीं करता है। फांसी हो सकती है (लटकने वाला सूचक)। उदाहरण: T* , T& , iterators, std::observer_ptr<T> , std::string_view , gsl::span<T>
  • स्वामी - मूल्य के जीवनकाल को नियंत्रित करता है। आमतौर पर शेड्यूल से पहले इसके मूल्य को हटा सकते हैं। उदाहरण: std::unique_ptr<T> , std::unique_ptr<T> , std::shared_ptr<T> std::vector<T> , std::string , gsl::owner<T*>

एक पॉइंटर निम्नलिखित राज्यों में से एक में हो सकता है:


  • स्टैक पर संग्रहीत मान पर इंगित करें
  • कुछ मालिक द्वारा "अंदर" एक मूल्य को इंगित करें
  • खाली हो (अशक्त)
  • हैंग (अमान्य)

संकेत और मूल्य


प्रत्येक सूचक के लिए नज़र रखी जाती है pset(p)- मूल्यों का वह सेट, जिसके लिए वह संकेत दे सकता है। किसी मान को हटाते समय, सभी में इसकी घटना psetद्वारा प्रतिस्थापित किया गया । जब एक पॉइंटर वैल्यू एक्सेस करते हैं ऐसा है pset(p)एक त्रुटि जारी करें।


 string_view s; // pset(s) = {null} { char a[100]; s = a; // pset(s) = {a} cout << s[0]; // OK } // pset(s) = {invalid} cout << s[0]; // ERROR: invalid ∈ pset(s) 

एनोटेशन का उपयोग करते हुए, आप कॉन्फ़िगर कर सकते हैं कि कौन से संचालन को मूल्य तक पहुंचने के संचालन माना जाएगा। डिफ़ॉल्ट रूप से: * , -> , [] , begin() , end()


कृपया ध्यान दें कि अमान्य सूचकांक तक पहुँच के समय ही चेतावनी जारी की जाती है। यदि मान हटा दिया जाता है, लेकिन कोई भी कभी इस पॉइंटर तक नहीं पहुंचता है, तो सब कुछ क्रम में है।


साइनपोस्ट और मालिक


यदि सूचक मालिक के भीतर निहित एक मूल्य को इंगित करता है फिर यह pset(p)=o


मालिकों को लेने वाले तरीके और कार्य, में विभाजित हैं:


  • मालिक मूल्य पहुंच संचालन। डिफ़ॉल्ट: * , -> , [] , begin() , end()
  • स्वामी के पास, v.clear() पॉइंटर्स, जैसे v.clear() तक पहुँच संचालन। डिफ़ॉल्ट रूप से, ये सभी अन्य नॉन-कास्ट ऑपरेशन हैं
  • स्वामी को स्वयं ऑपरेशन, गैर-अमान्य पॉइंटर्स, जैसे v.empty() । डिफ़ॉल्ट रूप से, ये सभी कांस्ट ऑपरेशन हैं।

पुरानी सामग्री के मालिक की घोषणा की मालिक को हटाने पर या अवैध संचालन के आवेदन पर।


ये नियम C ++ कोड में कई विशिष्ट बग्स का पता लगाने के लिए पर्याप्त हैं:


 string_view s; // pset(s) = {null} string name = "foo"; s = name; // pset(s) = {name'} cout << s[0]; // OK name = "bar"; // pset(s) = {invalid} cout << s[0]; // ERROR 

 vector<int> v = get_ints(); int* p = &v[5]; // pset(p) = {v'} v.push_back(42); // pset(p) = {invalid} cout << *p; // ERROR 

 std::string_view s = "foo"s; cout << s[0]; // ERROR // :       std::string_view s = "foo"s // pset(s) = {"foo"s '} ; // pset(s) = {invalid} 

 vector<int> v = get_ints(); for (auto i = v.begin(); i != v.end(); ++i) { // pset(i) = {v'} if (*i == 2) { v.erase(i); // pset(i) = {invalid} } // pset(i) = {v', invalid} } // ERROR: ++i for (auto i = v.begin(); i != v.end(); ) { if (*i == 2) i = v.erase(i); // OK else ++i; } 

 std::optional<std::vector<int>> get_data(); //   ,  get_data() != nullopt for (int value : *get_data()) // ERROR cout << value; // *get_data() —     for (int value : std::vector<int>(*get_data())) // OK cout << value; 

फ़ंक्शन मापदंडों के जीवनकाल को ट्रैक करना


जब हम C ++ में फ़ंक्शंस के साथ काम करना शुरू करते हैं जो पॉइंटर्स लौटाते हैं, तो हम केवल मापदंडों के जीवनकाल और रिटर्न वैल्यू के बीच संबंध के बारे में अनुमान लगा सकते हैं। यदि कोई फ़ंक्शन एक ही प्रकार के पॉइंटर्स को स्वीकार और वापस करता है, तो एक धारणा बनाई जाती है कि फ़ंक्शन को इनपुट मापदंडों में से एक से "रिटर्न" मिलता है:


 auto f(int* p, int* q) -> int*; // pset(ret) = {p', q'} auto g(std::string& s) -> char*; // pset(ret) = {s'} 

संदिग्ध कार्यों का आसानी से पता लगाया जाता है कि परिणाम कहीं से भी लें:


 std::reference_wrapper<int> get_data() { //    int i = 3; return {i}; // pset(ret) = {i'} } // pset(ret) = {invalid} 

चूंकि const T& मापदंडों के लिए एक अस्थायी मान पारित करना संभव है, इसलिए उन्हें ध्यान में नहीं रखा जाता है, जब तक कि परिणाम कहीं नहीं हो:


 template <typename T> const T& min(const T& x, const T& y); // pset(ret) = {x', y'} //    const T&- //        auto x = 10, y = 2; auto& bad = min(x, y + 1); // pset(bad) = {x, temp} // pset(bad) = {x, invalid} cout << bad; // ERROR 

 using K = std::string; using V = std::string; const V& find_or_default(const std::map<K, V>& m, const K& key, const V& def); // pset(ret) = {m', key', def'} std::map<K, V> map; K key = "foo"; const V& s = find_or_default(map, key, "none"); // pset(s) = {map', key', temp} ⇒ pset(s) = {map', key', invalid} cout << s; // ERROR 

यह भी माना जाता है कि यदि कोई फ़ंक्शन एक पॉइंटर (एक संदर्भ के बजाय) को स्वीकार करता है, तो इसे nullptr किया जा सकता है, और nullptr के साथ तुलना करने से पहले इस पॉइंटर का उपयोग नहीं किया जा सकता है।


लाइफ टाइम कंट्रोल निष्कर्ष


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


श्रोताओं से सवाल


क्या आजीवन समूह के चेक झूलने वाले बिंदुओं की अनुपस्थिति की गणितीय सटीक गारंटी प्रदान करते हैं?


सैद्धांतिक रूप से, यह (नए कोड में) वर्गों और कार्यों पर एनोटेशन का एक गुच्छा लटका देना संभव होगा, और बदले में कंपाइलर इस तरह की गारंटी देगा। लेकिन ये चेक 80:20 सिद्धांत के बाद विकसित किए गए थे, अर्थात, आप कम से कम नियमों का उपयोग करके और कम से कम एनोटेशन को लागू करते हुए अधिकांश त्रुटियों को पकड़ सकते हैं।


metaclasses


मेटाक्लास किसी तरह से उस वर्ग के कोड को पूरक करता है जिस पर इसे लागू किया जाता है, और कुछ वर्गों को संतुष्ट करने वाले वर्गों के समूह के नाम के रूप में भी कार्य करता है। उदाहरण के लिए, जैसा कि नीचे दिखाया गया है, interface मेटाक्लास आपके लिए सभी कार्यों को सार्वजनिक और विशुद्ध रूप से आभासी बना देगा।


पिछले साल, हर्ब सटर ने अपना पहला मेटाक्लस प्रोजेक्ट ( यहां देखें ) बनाया। तब से, वर्तमान प्रस्तावित सिंटैक्स बदल गया है।


शुरुआत के लिए, मेटाक्लासेस का उपयोग करने के लिए सिंटैक्स बदल गया है:


 //  interface Shape { int area() const; void scale_by(double factor); }; //  class(interface) Shape { … } 

यह लंबा हो गया है, लेकिन अब एक साथ कई class(meta1, meta2) लागू करने के लिए एक प्राकृतिक वाक्यविन्यास है: class(meta1, meta2)


मेटाक्लस विवरण


पहले, एक मेटाक्लास एक वर्ग को संशोधित करने के लिए नियमों का एक समूह था। अब मेटाक्लास एक कॉन्स्ट्रेक्स फ़ंक्शन है जो एक पुरानी कक्षा (कोड में घोषित) लेता है और एक नया बनाता है।


अर्थात्, फ़ंक्शन एक पैरामीटर लेता है - पुराने वर्ग के बारे में मेटा-जानकारी (पैरामीटर का प्रकार कार्यान्वयन पर निर्भर करता है), वर्ग तत्व (टुकड़े) बनाता है, और फिर __generate निर्देश का उपयोग करके उन्हें नए वर्ग के शरीर में जोड़ता है।


फ्रेगमेंट्स का निर्माण __fragment , __inject , idexpr(…) कंस्ट्रक्शन के उपयोग से किया जा सकता है। स्पीकर ने अपने उद्देश्य पर ध्यान नहीं देना पसंद किया, क्योंकि मानकीकरण समिति के सामने पेश किए जाने से पहले यह हिस्सा बदल जाएगा। नामों को स्वयं बदलने की गारंटी दी जाती है, इसे स्पष्ट करने के लिए विशेष रूप से डबल अंडरलाइनिंग जोड़ी गई थी। रिपोर्ट में जोर उन उदाहरणों पर दिया गया जो आगे चलते हैं।


इंटरफ़ेस


 template <typename T> constexpr void interface(T source) { // source    //     .     //  ~X,  X —   . __generate __fragment struct X { virtual ~X noexcept {} }; //    static_assert, compiler.require   //   constexpr-. //      . compiler.require(source.variables().empty(), "interfaces may not contain data members"); // member_functions(), ,  tuple<…>,   for... for... (auto f : source.member_functions()) { // ,   —   / compiler.require(!f.is_copy() && !f.is_move(), "interfaces may not copy or move; consider a virtual clone()"); //   public   if (!f.has_default_access()) f.make_public(); // (1) // ,       protected/private compiler.require(f.is_public(), "interface functions must be public"); //     f.make_pure_virtual(); // (2) //   f     __generate f; } } 

आप सोच सकते हैं कि तर्ज पर (1) और (2) हम मूल वर्ग को संशोधित करते हैं, लेकिन नहीं। कृपया ध्यान दें कि हम मूल कक्षा के कार्यों को कॉपी करने के साथ पुन: व्यवस्थित करते हैं, इन कार्यों को संशोधित करते हैं, और फिर उन्हें एक नए वर्ग में सम्मिलित करते हैं।


मेटाक्लास आवेदन:


 class(interface) Shape { int area() const; void scale_by(double factor); }; //  : class Shape { public: virtual ~Shape noexcept {} public: virtual int area() const = 0; public: virtual void scale_by(double factor) = 0; }; 

म्यूटेक्स डिबगिंग


मान लीजिए कि हमारे पास म्यूटेक्स द्वारा संरक्षित गैर-थ्रेड सुरक्षित डेटा है। डिबगिंग की सुविधा हो सकती है, यदि डिबग असेंबली में, प्रत्येक कॉल पर, यह जांचा जाता है कि क्या वर्तमान प्रक्रिया ने इस म्यूटेक्स को लॉक कर दिया है। ऐसा करने के लिए, एक साधारण TestableMutex वर्ग लिखा गया था:


 class TestableMutex { public: void lock() { m.lock(); id = std::this_thread::get_id(); } void unlock() { id = std::thread::id{}; m.unlock(); } bool is_held() { return id == std::this_thread::get_id(); } private: std::mutex m; std::atomic<std::thread::id> id; }; 

इसके अलावा, हमारे MyData वर्ग में हम हर सार्वजनिक क्षेत्र को पसंद करेंगे


 vector<int> v; 

+ गेट्टर से बदलें:


 private: vector<int> v_; public: vector<int>& v() { assert(m_.is_held()); return v_; } 

कार्यों के लिए, कोई भी समान परिवर्तन कर सकता है।


मैक्रो और कोड जनरेशन का उपयोग करके ऐसे कार्यों को हल किया जाता है। हर्ब सटर ने मैक्रोज़ पर युद्ध की घोषणा की: वे असुरक्षित हैं, शब्दार्थ, नाम स्थान आदि की उपेक्षा करते हैं। मेटाक्लासेस पर समाधान कैसा दिखता है:


 constexpr void guarded_with_mutex() { __generate __fragment class { TestableMutex m_; // lock, unlock } } template <typename T, typename U> constexpr void guarded_member(T type, U name) { auto field = …; __generate field; auto getter = …; __generate getter; } template <typename T> constexpr void guarded(T source) { guarded_with_mutex(); for... (auto o : source.member_variables()) { guarded_member(o.type(), o.name()); } } 

इसका उपयोग कैसे करें:


 class(guarded) MyData { vector<int> v; Widget* w; }; MyData& x = findData("foo"); xv().clear(); // assertion failed: m_.is_held() 

अभिनेता


ठीक है, भले ही हमने किसी वस्तु को म्यूटेक्स के साथ संरक्षित किया हो, अब सब कुछ थ्रेड सुरक्षित है, शुद्धता के दावे नहीं हैं। लेकिन अगर किसी वस्तु को अक्सर समानांतर में कई थ्रेड्स द्वारा एक्सेस किया जा सकता है, तो म्यूटेक्स ओवरलोड हो जाएगा, और इसे लेने के लिए एक बड़ा ओवरहेड होगा।


छोटी गाड़ी म्यूटेक्स की समस्या का मूल समाधान अभिनेताओं की अवधारणा है, जब किसी ऑब्जेक्ट में एक अनुरोध कतार होती है, तो ऑब्जेक्ट के सभी कॉल कतारबद्ध होते हैं और एक विशेष धागे में एक के बाद एक निष्पादित होते हैं।


सक्रिय वर्ग को यह सब लागू करने दें - वास्तव में, एक थ्रेड पूल / एक एकल थ्रेड के साथ निष्पादक। ठीक है, मेटाक्लिप्स डुप्लिकेट कोड से छुटकारा पाने और सभी कार्यों को कतार में लाने में मदद करेंगे:


 class(active) ImageFilter { public: ImageFilter(std::function<void(Buffer*)> w) : work(std::move(w)) {} void apply(Buffer* b) { work(b); } private: std::function<void(Buffer*)> work; } //  : class ImageFilter { public: ImageFilter(std::function<void(Buffer*)> w) : work(std::move(w)) {} void apply(Buffer* b) { a.send([=] { work(b); }).join(); } private: std::function<void(Buffer*)> work; Active a; //   ,     work } 

 class(active) log { std::fstream f; public: void info(…) { f << …; } }; 

संपत्ति


लगभग सभी आधुनिक प्रोग्रामिंग भाषाओं में गुण हैं, और जिन्होंने कभी भी C ++: Qt, C ++ / CLI, सभी प्रकार के बदसूरत मैक्रोज़ के आधार पर उन्हें लागू नहीं किया। हालांकि, उन्हें सी ++ मानक में कभी नहीं जोड़ा जाएगा, क्योंकि वे खुद को बहुत ही संकीर्ण विशेषताओं के रूप में मानते हैं, और हमेशा यह आशा थी कि कुछ प्रस्ताव उन्हें एक विशेष मामले के रूप में लागू करेंगे। ठीक है, वे मेटाक्लस पर लागू किए जा सकते हैं!


 //  class X { public: class(property<int>) WidthClass { } width; }; //  class X { public: class WidthClass { int value; int get() const; void set(const int& v); void set(int&& v); public: WidthClass(); WidthClass(const int& v); WidthClass& operator=(const int& v); operator int() const; //   move! WidthClass(int&& v); WidthClass& operator=(int&& v); } width; }; 

आप अपना खुद का गेटटर और सेटर सेट कर सकते हैं:


 class Date { public: class(property<int>) MonthClass { int month; auto get() { return month; } void set(int m) { assert(m > 0 && m < 13); month = m; } } month; }; Date date; date.month = 15; // assertion failed 

आदर्श रूप से, मैं property int month { … } लिखना चाहता हूं, लेकिन यहां तक ​​कि इस तरह के कार्यान्वयन से संपत्तियों का आविष्कार करने वाले C ++ एक्सटेंशन के चिड़ियाघर को बदल दिया जाएगा।


मेटाक्लस निष्कर्ष


मेटाक्लस पहले से ही जटिल भाषा के लिए एक बड़ी नई विशेषता है। क्या यह इसके लायक है? यहाँ उनके कुछ लाभ हैं:


  • प्रोग्रामर अपने इरादों को अधिक स्पष्ट रूप से व्यक्त करें (मैं अभिनेता लिखना चाहता हूं)
  • कोड दोहराव को कम करना और कुछ पैटर्न के बाद कोड के विकास और रखरखाव को सरल बनाना
  • सामान्य त्रुटियों के कुछ समूहों को हटा दें (यह एक बार सभी सूक्ष्मताओं की देखभाल करने के लिए पर्याप्त होगा)
  • मैक्रोज़ से छुटकारा पाने की अनुमति दें? (हर्ब सटर बहुत जुझारू है)

श्रोताओं से सवाल


मेटाक्लासेस को डिबग कैसे करें?


कम से कम क्लैंग के लिए, एक आंतरिक कार्य है जिसे अगर कहा जाता है, तो संकलन के समय कक्षा की वास्तविक सामग्री को मुद्रित करेगा, अर्थात, सभी मेटाक्लस को लागू करने के बाद क्या प्राप्त होता है।


यह कहा जाता था कि यह मेटाक्लासेस में स्वैप और हैश जैसे गैर-सदस्यों की घोषणा करने में सक्षम है। वह कहां गई?


सिंटैक्स को और विकसित किया जाएगा।


यदि मानकीकरण के लिए पहले से ही अवधारणाएं अपनाई गई हैं तो हमें मेटाक्लस की आवश्यकता क्यों है?


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

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


All Articles