
أضاف معيار C ++ 17 ميزة جديدة إلى اللغة: خصم وسيطة قالب الفصل الدراسي (CTAD) . جنبا إلى جنب مع ميزات جديدة في C ++ ، وأضاف تقليديا طرق جديدة لاطلاق النار أطرافهم. في هذه المقالة ، سوف نفهم ماهية CTAD ، وما الذي تستخدمه ، وكيف تبسط الحياة ، وما هي عيوبها.
لنبدأ من بعيد
أذكر ما هو كل ما يخص " وسيطة الخصم" وما هو المقصود به. إذا كنت تشعر بالثقة الكافية مع قوالب C ++ ، يمكنك تخطي هذا القسم والمتابعة على الفور إلى التالي.
قبل C ++ 17 ، يتم تطبيق إخراج معلمات القالب فقط على قوالب الوظائف. عند إنشاء قالب دالة ، لا يجوز لك تحديد وسيطات القوالب بشكل صريح يمكن استنتاجها من أنواع وسائط الدالة الفعلية. قواعد الاستنتاج معقدة للغاية ، فهي مكرسة للقسم بأكمله 17.9.2 في المعيار [temp.deduct] (أشير فيما يلي إلى الإصدار المتاح مجانًا من مسودة المعيار ؛ في الإصدارات المستقبلية ، قد يتغير ترقيم القسم ، لذلك أوصي بالبحث عن طريق الكود ذاكري المحدد في الأقواس المربعة).
لن نحلل بالتفصيل جميع تعقيدات هذه القواعد ؛ فهناك حاجة فقط إلى مطوري برامج التحويل البرمجي. للاستخدام العملي ، يكفي أن تتذكر قاعدة بسيطة: يمكن للمترجم أن يستخلص بشكل مستقل وسيطات قالب الوظيفة ، إذا كان يمكن القيام بذلك بشكل لا لبس فيه استنادًا إلى المعلومات المتاحة. عند اشتقاق أنواع معلمات القالب ، يتم تطبيق التحويلات القياسية كما هو الحال عند استدعاء دالة عادية ( يتم تجاهل const من الأنواع الحرفية ، يتم تقليل الصفائف إلى مؤشرات ، ويتم تقليل مراجع الوظائف إلى مؤشرات دالة ، وما إلى ذلك).
template <typename T> void func(T t) {
كل هذا يبسط استخدام قوالب الوظائف ، ولكن ، للأسف ، غير قابل للتطبيق تمامًا على قوالب الفئة. عند إنشاء قوالب للفصول ، يجب تحديد جميع معلمات القالب غير الافتراضية بشكل صريح. نظرًا لهذه الخاصية غير السارة ، ظهرت مجموعة كاملة من الوظائف المجانية مع البادئة make في المكتبة القياسية: make_unique ، make_shared ، make_pair ، make_tuple ، إلخ.
الجديد في C ++ 17
في المعيار الجديد ، من خلال القياس مع معلمات قوالب الوظائف ، يتم استخلاص معلمات قوالب الفئة من وسيطات المُنشئات المدعوين:
std::pair pr(false, 45.67);
تجدر الإشارة إلى قيود CTAD التي تنطبق في وقت الإصدار C ++ 17 (ربما ستتم إزالة هذه القيود في الإصدارات المستقبلية من المعيار):
- CTAD لا يعمل مع الأسماء المستعارة للقالب:
template <typename X> using PairIntX = std::pair<int, X>; PairIntX p{1, true};
- لا يسمح CTAD بالإخراج الجزئي للوسيطات (كيف يعمل هذا لخصم وسيطة قالب منتظم):
std::pair p{1, 5};
أيضًا ، لن يتمكن المحول البرمجي من استنتاج أنواع معلمات القوالب التي لا ترتبط صراحةً بأنواع وسيطات المُنشئ. أبسط مثال على ذلك هو مُنشئ حاوية يقبل زوجًا من التكرارات:
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 ، على الرغم من أننا المطورين نعرف بالضبط كيفية الحصول عليه. لإعلام المترجم بكيفية إخراج أنواع غير مرتبطة مباشرة ، ظهر إنشاء لغة جديد في 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()};
بالمناسبة ، لا يجب أن يكون دليل الخصم عبارة عن قالب:
template<class T> struct S { S(T); }; S(char const*) -> S<std::string>; S s{"hello"};
خوارزمية 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); };
- إذا لم يتم تعريف النوع C ، أو لم يتم تحديد أي مُنشئين ، فإن القواعد المذكورة أعلاه تنطبق على المُنشئ الافتراضي C () .
- يتم إنشاء وظيفة دمية إضافية لـ C © constructor ؛ لذلك ، توصلوا إلى اسم خاص: copy deduction مرشح .
- لكل دليل خصم ، يتم إنشاء دالة وهمية Fi أيضًا مع معلمات القالب وسيطات دليل الخصم وقيمة الإرجاع المقابلة للنوع الموجود على يمين -> في دليل الخصم (في التعريف الرسمي يطلق عليه معرف القالب البسيط ).
الزائفة رمز:
template <typename T, typename V> C(T, V) -> C<typename DT<T>, typename DT<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};
أخيرًا ، إذا كان من الممكن اختيار الوظيفة الوهمية الأكثر ملاءمة ، فسيتم تحديد المنشئ أو دليل الاستنتاج المقابل. إذا لم يكن هناك منها مناسبة ، أو كان هناك العديد منها مناسبة على قدم المساواة ، فإن المترجم يبلغ عن خطأ.
المزالق
يستخدم CTAD لتهيئة الكائنات ، والتهيئة هي تقليديًا جزء مربك جدًا من لغة C ++. مع إضافة التهيئة الموحدة في C ++ 11 ، زادت طرق إطلاق النار على ساقك فقط. الآن يمكنك استدعاء المنشئ لكائن مع كل من الأقواس الدائرية والمجعدة. في كثير من الحالات ، يعمل كلا الخيارين بنفس الطريقة ، ولكن ليس دائمًا:
std::vector v1{8, 15};
حتى الآن ، يبدو أن كل شيء منطقي تمامًا: استدعاء v1 و v3 المُنشئ الذي يأخذ std::initializer_list<int>
، يُستنتج int من المعلمات ؛ يتعذر على v4 إيجاد مُنشئ يأخذ معلمة واحدة فقط من النوع int . ولكن هذه لا تزال الزهور والتوت في الجبهة:
std::vector v5{"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 "خصم وسيطة قالب الفصل للجميع"