قالب حجة فئة الفصل


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


لنبدأ من بعيد


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


قبل C ++ 17 ، يتم تطبيق إخراج معلمات القالب فقط على قوالب الوظائف. عند إنشاء قالب دالة ، لا يجوز لك تحديد وسيطات القوالب بشكل صريح يمكن استنتاجها من أنواع وسائط الدالة الفعلية. قواعد الاستنتاج معقدة للغاية ، فهي مكرسة للقسم بأكمله 17.9.2 في المعيار [temp.deduct] (أشير فيما يلي إلى الإصدار المتاح مجانًا من مسودة المعيار ؛ في الإصدارات المستقبلية ، قد يتغير ترقيم القسم ، لذلك أوصي بالبحث عن طريق الكود ذاكري المحدد في الأقواس المربعة).


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


template <typename T> void func(T t) { // ... } int some_func(double d) { return static_cast<int>(d); } int main() { const int i = 123; func(i); // func<int> char arr[] = "Some text"; func(arr); // func<char *> func(some_func); // func<int (*)(double)> return 0; } 

كل هذا يبسط استخدام قوالب الوظائف ، ولكن ، للأسف ، غير قابل للتطبيق تمامًا على قوالب الفئة. عند إنشاء قوالب للفصول ، يجب تحديد جميع معلمات القالب غير الافتراضية بشكل صريح. نظرًا لهذه الخاصية غير السارة ، ظهرت مجموعة كاملة من الوظائف المجانية مع البادئة make في المكتبة القياسية: make_unique ، make_shared ، make_pair ، make_tuple ، إلخ.


 //  auto tup1 = std::tuple<int, char, double>(123, 'a', 40.0); //   auto tup2 = std::make_tuple(123, 'a', 40.0); 

الجديد في C ++ 17


في المعيار الجديد ، من خلال القياس مع معلمات قوالب الوظائف ، يتم استخلاص معلمات قوالب الفئة من وسيطات المُنشئات المدعوين:


 std::pair pr(false, 45.67); // std::pair<bool, double> std::tuple tup(123, 'a', 40.0); // std::tuple<int, char, double> std::less l; // std::less<void>,     std::less<> l template <typename T> struct A { A(T,T); }; auto y = new A{1, 2}; //  A<int> auto lck = std::lock_guard(mtx); // std::lock_guard<std::mutex> std::copy_n(vi1, 3, std::back_insert_iterator(vi2)); //       template <typename T> struct F { F(T); } std::for_each(vi.begin(), vi.end(), Foo([&](int i) {...})); // F<lambda> 

تجدر الإشارة إلى قيود CTAD التي تنطبق في وقت الإصدار C ++ 17 (ربما ستتم إزالة هذه القيود في الإصدارات المستقبلية من المعيار):


  • CTAD لا يعمل مع الأسماء المستعارة للقالب:

 template <typename X> using PairIntX = std::pair<int, X>; PairIntX p{1, true}; //   

  • لا يسمح CTAD بالإخراج الجزئي للوسيطات (كيف يعمل هذا لخصم وسيطة قالب منتظم):

 std::pair p{1, 5}; // OK std::pair<double> q{1, 5}; // ,   std::pair<double, int> r{1, 5}; // OK 

أيضًا ، لن يتمكن المحول البرمجي من استنتاج أنواع معلمات القوالب التي لا ترتبط صراحةً بأنواع وسيطات المُنشئ. أبسط مثال على ذلك هو مُنشئ حاوية يقبل زوجًا من التكرارات:


 template <typename T> struct MyVector { template <typename It> MyVector(It from, It to); }; std::vector<double> dv = {1.0, 3.0, 5.0, 7.0}; MyVector v2{dv.begin(), dv.end()}; //     T   It 

النوع لا يرتبط مباشرة بـ T ، على الرغم من أننا المطورين نعرف بالضبط كيفية الحصول عليه. لإعلام المترجم بكيفية إخراج أنواع غير مرتبطة مباشرة ، ظهر إنشاء لغة جديد في C ++ 17 - دليل الاستنتاج ، الذي سنناقشه في القسم التالي.


أدلة التفاني


على سبيل المثال أعلاه ، سيبدو دليل الخصم كما يلي:


 template <typename It> MyVector(It, It) -> MyVector<typename std::iterator_traits<It>::value_type>; 

نحن هنا نقول للمترجم أنه بالنسبة std::iterator_traits<It>::value_type ذو المعلمتين من نفس النوع ، يمكنك تحديد نوع T باستخدام std::iterator_traits<It>::value_type البناء std::iterator_traits<It>::value_type . يرجى ملاحظة أن أدلة الاستنباط تقع خارج تعريف الفصل ، وهذا يسمح لك بتخصيص سلوك الفئات الخارجية ، بما في ذلك الفئات من مكتبة C ++ القياسية.


ويرد وصف رسمي لتركيب أدلة الاستنتاج في C ++ Standard 17 في القسم 17.10 [temp.deduct.guide] :


 [explicit] template-name (parameter-declaration-clause) -> simple-template-id; 

الكلمة المفتاحية الصريحة قبل دليل الاستنتاج تحظر استخدامه مع تهيئة قائمة النسخ :


 template <typename It> explicit MyVector(It, It) -> MyVector<typename std::iterator_traits<It>::value_type>; std::vector<double> dv = {1.0, 3.0, 5.0, 7.0}; MyVector v2{dv.begin(), dv.end()}; //  MyVector v3 = {dv.begin(), dv.end()}; //   

بالمناسبة ، لا يجب أن يكون دليل الخصم عبارة عن قالب:


 template<class T> struct S { S(T); }; S(char const*) -> S<std::string>; S s{"hello"}; // S<std::string> 

خوارزمية CTAD مفصلة


يتم شرح القواعد الرسمية لاشتقاق وسيطات قالب الفئة بالتفصيل في الفقرة 16.3.1.8 [over.match.class.deduct] من معيار C ++ 17. دعونا نحاول معرفة ذلك.


لذلك ، لدينا نوع القالب C الذي يتم تطبيق CTAD عليه. لاختيار أي مُنشئ ومع أي معلمات تستدعي ، بالنسبة لـ C ، يتم تشكيل الكثير من وظائف القوالب وفقًا للقواعد التالية:


  • لكل منشئ Ci ، يتم إنشاء وظيفة قالب Fi وهمية. معلمات قالب Fi هي معلمات C ، تليها معلمات قالب Ci (إن وجدت) ، بما في ذلك المعلمات ذات القيم الافتراضية. تتوافق أنواع معلمات الدالة Fi مع أنواع المعلمات الخاصة بمنشئ Ci . يُرجع دالة وهمية من النوع Fi مع وسيطات تتطابق مع معلمات القالب C.

الزائفة رمز:


 template <typename T, typename U> class C { public: template <typename V, typename W = A> C(V, W); }; //    template <typename T, typename U, typename V, typename W = A> C<T, U> Fi(V, W); 

  • إذا لم يتم تعريف النوع C ، أو لم يتم تحديد أي مُنشئين ، فإن القواعد المذكورة أعلاه تنطبق على المُنشئ الافتراضي C () .
  • يتم إنشاء وظيفة دمية إضافية لـ C © constructor ؛ لذلك ، توصلوا إلى اسم خاص: copy deduction مرشح .
  • لكل دليل خصم ، يتم إنشاء دالة وهمية Fi أيضًا مع معلمات القالب وسيطات دليل الخصم وقيمة الإرجاع المقابلة للنوع الموجود على يمين -> في دليل الخصم (في التعريف الرسمي يطلق عليه معرف القالب البسيط ).

الزائفة رمز:


 template <typename T, typename V> C(T, V) -> C<typename DT<T>, typename DT<V>>; //    template <typename T, typename V> C<typename DT<T>, typename DT<V>> Fi(T,V); 

علاوة على ذلك ، بالنسبة لمجموعة الدال الوهمية الناتجة ، يتم تطبيق القواعد المعتادة لإخراج معلمات القالب ودقة التحميل الزائد مع استثناء واحد: عندما يتم استدعاء وظيفة الدمية بقائمة تهيئة تتكون من معلمة واحدة من النوع cv U ، حيث U تخصص C أو نوع موروث من تخصص C (فقط في الحالة ، سأوضح أن cv == const متقلبة ؛ فهذا السجل يعني أن الأنواع U و const U و U المضطربة و U و const المتقلبة U تعامل بنفس الطريقة) ، والقاعدة التي تعطي الأولوية C(std::initializer_list<>) (يتم تخطيها للحصول على تفاصيل قائمة initia يمكن العثور على lization في الفقرة 16.3.1.7 [over.match.list] من C ++ Standard 17). مثال:


 std::vector v1{1, 2}; // std::vector<int> std::vector v2{v1}; // std::vector<int>,   std::vector<std::vector<int>> 

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


المزالق


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


 std::vector v1{8, 15}; // [8, 15] std::vector v2(8, 15); // [15, 15, … 15] (8 ) std::vector v3{8}; // [8] std::vector v4(8); //   

حتى الآن ، يبدو أن كل شيء منطقي تمامًا: استدعاء v1 و v3 المُنشئ الذي يأخذ std::initializer_list<int> ، يُستنتج int من المعلمات ؛ يتعذر على v4 إيجاد مُنشئ يأخذ معلمة واحدة فقط من النوع int . ولكن هذه لا تزال الزهور والتوت في الجبهة:


 std::vector v5{"hi", "world"}; // [“hi”, “world”] std::vector v6("hi", "world"); // ?? 

v5 ، كما هو متوقع ، ستكون من النوع std::vector<const char*> وستتم تهيئتها باستخدام عنصرين ، لكن السطر التالي يقوم بشيء مختلف تمامًا. بالنسبة للمتجه ، يوجد مُنشئ واحد فقط يأخذ معلمتين من نفس النوع:


 template< class InputIt > vector( InputIt first, InputIt last, const Allocator& alloc = Allocator() ); 

بفضل دليل الاستنباط الخاص بـ std::vector ستتم معاملة "hi" و "world" على أنهما متكررتان ، وستتم إضافة جميع العناصر الموجودة "بين" إلى متجه من النوع std::vector<char> . إذا كنا محظوظين وكانت ثوابت السلسلة اثنين في الذاكرة على التوالي ، فسوف تسقط ثلاثة عناصر في المتجه: 'h' و 'i' و \ x00 '، لكن على الأرجح ، سيؤدي هذا الرمز إلى انتهاك لحماية الذاكرة وتعطل البرنامج.


المواد المستخدمة


مسودة المعيار C ++ 17
CTAD
CppCon 2018: Stephan T. Lavavej "خصم وسيطة قالب الفصل للجميع"

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


All Articles