[CppCon 2018] Herb Sutter: نحو أبسط وأقوى C ++


في كلمته في CppCon 2018 ، قدم هيرب سوتر للجمهور إنجازاته في اتجاهين. بادئ ذي بدء ، إنه التحكم في عمر المتغيرات (Lifetime) ، والذي يسمح باكتشاف فئات كاملة من الأخطاء في مرحلة التجميع. ثانيًا ، هذا اقتراح محدث حول metaclasses ، والذي سيسمح بتجنب ازدواج التعليمات البرمجية ، بمجرد وصف سلوك فئة الفئة ثم ربطها بفصول محددة بسطر واحد.


مقدمة: المزيد = أسهل ؟!


تسمع اتهامات C ++ بأن المعيار ينمو بلا معنى ولا رحمة. ولكن حتى أكثر المحافظين المتحمسين لن يجادلوا في أن مثل هذه الإنشاءات الجديدة مثل Range-for (دورة التجميع) والسيارات (على الأقل بالنسبة للمكررات) تجعل الشفرة أبسط. يمكنك تطوير معايير تقريبية يجب أن تفي بها (على الأقل واحدة ، جميعها بشكل مثالي) امتدادات اللغة الجديدة من أجل تبسيط الشفرة في الممارسة:


  1. قم بتقليل وتبسيط التعليمات البرمجية وإزالة التعليمات البرمجية المكررة (نطاق - تلقائي - لامدا - ميتاكلاس)
  2. تسهيل كتابة الرمز الآمن ومنع الأخطاء والحالات الخاصة (المؤشرات الذكية وأوقات الحياة)
  3. استبدال الميزات القديمة والأقل وظيفية بالكامل (typedef → باستخدام)

يحدد Herb Sutter "لغة C ++ الحديثة" - وهي مجموعة فرعية من الميزات التي تتوافق مع معايير الترميز الحديثة (مثل إرشادات C ++ الأساسية ) ، وتعتبر المعيار الكامل بمثابة "وضع التوافق" ، والذي لا يحتاج الجميع إلى معرفته. وبناء على ذلك ، إذا لم ينمو "C ++ الحديث" ، فكل شيء على ما يرام.


التحقق من عمر المتغيرات (مدى الحياة)


تتوفر مجموعة التحقق مدى الحياة الجديدة الآن كجزء من مدقق الإرشادات الأساسية لـ Clang و Visual C ++. الهدف ليس تحقيق الدقة والدقة المطلقة ، كما هو الحال في Rust ، ولكن لإجراء فحوصات بسيطة وسريعة داخل الوظائف الفردية.


المبادئ الأساسية للتحقق


من وجهة نظر تحليل مدى الحياة ، تنقسم الأنواع إلى 3 فئات:


  • القيمة هي ما يمكن أن يشير إليه المؤشر.
  • المؤشر - يشير إلى القيمة ، لكنه لا يتحكم في عمرها. قد تكون معلقة (مؤشر متدلي). أمثلة: T* ، T& ، iterators، std::observer_ptr<T> ، std::string_view ، gsl::span<T>
  • المالك - يتحكم في عمر القيمة. عادة يمكن حذف قيمته قبل الموعد المحدد. أمثلة: std::unique_ptr<T> ، std::shared_ptr<T> ، std::vector<T> ، std::string ، gsl::owner<T*>

يمكن أن يكون المؤشر في إحدى الحالات التالية:


  • أشر إلى قيمة مخزنة على المكدس
  • أشر إلى قيمة تحتوي على "من الداخل" من قبل بعض المالك
  • كن فارغًا (فارغ)
  • تعليق (غير صالح)

المؤشرات والقيم


لكل مؤشر p يتم تتبع pset(p) - مجموعة القيم التي قد تشير إليها. عند حذف قيمة ، حدوثها في كل شيء pset تم استبداله بـ غيرصالح . عند الوصول إلى قيمة المؤشر p مثل هذا غيرصالحsetpset(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() .


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


معالم وأصحاب


إذا كان المؤشر p يشير إلى قيمة موجودة داخل المالك o ثم هذا pset(p)=o .


تنقسم الأساليب والوظائف التي تأخذ أصحابها إلى:


  • عمليات الوصول إلى قيمة المالك. الافتراضي: * ، -> ، [] ، begin() ، end()
  • عمليات الوصول إلى المالك نفسه ، مؤشرات v.clear() ، مثل v.clear() . بشكل افتراضي ، هذه كلها عمليات أخرى غير ثابتة
  • عمليات الوصول إلى المالك نفسه ، مؤشرات غير v.empty() ، مثل 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.


خاتمة التحكم في وقت الحياة


أكرر أن Lifetime ليس مقترحًا لمعيار C ++ حتى الآن ، ولكنه محاولة جريئة لتنفيذ عمليات التحقق مدى الحياة في C ++ ، حيث ، على عكس Rust ، على سبيل المثال ، لم يكن هناك أي تعليقات توضيحية مقابلة. في البداية ، سيكون هناك العديد من الإيجابيات الخاطئة ، ولكن بمرور الوقت ، ستتحسن الاستدلال.


أسئلة من الجمهور


هل توفر فحوصات مجموعة Lifetime ضمانًا رياضيًا دقيقًا لغياب مؤشرات التعلق؟


نظريًا ، سيكون من الممكن (في الكود الجديد) تعليق مجموعة من التعليقات التوضيحية على الفئات والوظائف ، وفي المقابل سيقدم المترجم مثل هذه الضمانات. ولكن تم تطوير عمليات التحقق هذه وفقًا لمبدأ 80:20 ، أي أنه يمكنك التقاط معظم الأخطاء باستخدام عدد صغير من القواعد وتطبيق الحد الأدنى من التعليقات التوضيحية.


ميتاكلاس


يكمل metaclass بطريقة ما كود الفئة التي يتم تطبيقه عليها ، ويعمل أيضًا كاسم لمجموعة من الفئات التي تلبي شروطًا معينة. على سبيل المثال ، كما هو موضح أدناه ، ستجعل interface الأولية interface جميع الوظائف عامة وظاهرية بحتة لك.


في العام الماضي ، قام هيرب سوتر بأول مشروع للطبقة الفوقية ( انظر هنا ). منذ ذلك الحين ، تغيرت الصيغة المقترحة الحالية.


بالنسبة للمبتدئين ، تم تغيير بنية استخدام metaclasses:


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

لقد أصبحت أطول ، ولكن الآن هناك بنية طبيعية لتطبيق العديد من metaclasses في وقت واحد: class(meta1, meta2) .


وصف Metaclass


سابقًا ، كان metaclass عبارة عن مجموعة من القواعد لتعديل فئة. الآن metaclass هي دالة constexpr تأخذ فئة قديمة (معلن عنها في الكود) وتنشئ فئة جديدة.


أي أن الوظيفة تأخذ معلمة واحدة - المعلومات الوصفية عن الفئة القديمة (يعتمد نوع المعلمة على التنفيذ) ، وإنشاء عناصر الفئة (الأجزاء) ، ثم إضافتها إلى نص الفئة الجديدة باستخدام التعليمة __generate .


يمكن إنشاء __fragment باستخدام __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) نقوم بتعديل الفصل الأصلي ، ولكن لا. يرجى ملاحظة أننا نقوم بتكرار وظائف الفصل الأصلي مع النسخ ، وتعديل هذه الوظائف ، ثم إدراجها في فصل جديد.


تطبيق Metaclass:


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

التصحيح Mutex


افترض أن لدينا بيانات آمنة غير خيطية محمية بواسطة كائن مزامنة. يمكن تسهيل التصحيح إذا تم التحقق ، في تجميع تصحيح الأخطاء ، في كل مكالمة ، مما إذا كانت العملية الحالية قد أقفلت هذا التعديل. للقيام بذلك ، تم كتابة فئة 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; 

الاستبدال بـ + getter:


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

بالنسبة للوظائف ، يمكن للمرء أيضًا إجراء تحويلات مماثلة.


يتم حل هذه المهام باستخدام وحدات الماكرو وإنشاء التعليمات البرمجية. أعلن Herb Sutter الحرب على وحدات الماكرو: فهي غير آمنة ، وتتجاهل الدلالات ، ومساحات الأسماء ، إلخ. كيف يبدو الحل على metaclasses:


 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() 

الفاعل


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


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


دع الفصل النشط يحتوي على تطبيق لكل هذا - في الواقع ، تجمع / منفذ تنفيذ لمؤشر ترابط واحد. حسنًا ، سوف تساعد metaclasses في التخلص من التعليمات البرمجية المكررة وقائمة انتظار جميع العمليات:


 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 ، وجميع أنواع وحدات الماكرو القبيحة. ومع ذلك ، لن تتم إضافتها مطلقًا إلى معيار C ++ ، نظرًا لأنها تعتبر نفسها ميزات ضيقة جدًا ، وكان هناك دائمًا أمل في أن يقوم بعض الاقتراح بتنفيذها كحالة خاصة. حسنًا ، يمكن تنفيذها على metaclasses!


 //  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 ++ التي تخترع الخصائص.


خاتمة Metaclass


Metaclasses هي ميزة جديدة كبيرة للغة معقدة بالفعل. هل يستحق ذلك؟ فيما يلي بعض فوائدها:


  • دع المبرمجين يعبرون عن نواياهم بشكل أكثر وضوحًا (أريد أن أكتب الممثل)
  • تقليل تكرار التعليمات البرمجية وتبسيط تطوير وصيانة التعليمات البرمجية التي تتبع أنماطًا معينة
  • تخلص من بعض مجموعات الأخطاء الشائعة (سيكون كافيا لرعاية جميع التفاصيل الدقيقة مرة واحدة)
  • السماح للتخلص من وحدات الماكرو؟ (Herb Sutter محارب للغاية)

أسئلة من الجمهور


كيفية تصحيح metaclasses؟


على الأقل بالنسبة لـ Clang ، هناك وظيفة ذاتية ، إذا تم استدعاؤها ، ستطبع المحتويات الفعلية للفصل أثناء التجميع ، أي ما يتم الحصول عليه بعد تطبيق جميع الصفائح المعدنية.


كان يقال أنه قادر على إعلان غير الأعضاء مثل المبادلة والتجزئة في metaclass. اين ذهبت؟


سيتم تطوير بناء الجملة.


لماذا نحتاج إلى metaclass إذا تم بالفعل اعتماد المفاهيم للتوحيد القياسي؟


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

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


All Articles