c++ - لماذا ترتيب استبدال وسيطة القالب مهم؟



templates c++11 (1)

كما ذكر C ++ 14 صراحة أن ترتيب استبدال وسيطة القالب محدد بشكل جيد ؛ وبشكل أكثر تحديدًا ، سيتم ضمان المضي قدمًا في "ترتيب معجمية ووقفها عندما يؤدي استبدال إلى فشل الاستنتاج.

بالمقارنة مع C ++ 11 ، سيكون من الأسهل كتابة SFINAE -code الذي يتألف من قاعدة تعتمد على آخر في C ++ 14 ، وسوف نتحرك أيضًا بعيدًا عن الحالات التي قد يؤدي فيها الترتيب غير المحدود لاستبدال النماذج إلى جعل تطبيقنا بالكامل يعاني من غير معروف-السلوك.

ملاحظة : من المهم ملاحظة أن السلوك الموصوف في C ++ 14 كان دائمًا السلوك المقصود ، حتى في C ++ 11 ، فقط أنه لم يتم صياغته بطريقة واضحة.

ما هو الأساس المنطقي وراء هذا التغيير؟

يمكن العثور على السبب الأصلي وراء هذا التغيير في تقرير عيب تم تقديمه في الأصل بواسطة دانيال كروغلر :

المزيد من التوضيح

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

فشل تبديل ليس خطأ ، ولكن مجرد .. "عذرًا ، لم ينجح هذا .. الرجاء الانتقال" .

تكمن المشكلة في أنه لا يتم البحث عن الأنواع والتعبيرات غير الصالحة إلا في السياق المباشر للإحلال.

14.8.2 - خصم الوسيطة [temp.deduct] - [temp.deduct]

8 إذا أدى استبدال في نوع أو تعبير غير صحيح ، فاخفق في عملية الخصم. نوع أو تعبير غير صحيح هو نوع سيبدو غير صحيح إذا تمت كتابته باستخدام الوسيطات البديلة.

[ ملاحظة: يتم إجراء فحص الوصول كجزء من عملية الاستبدال. - note note ]

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

[ ملاحظة: يمكن أن يؤدي تقييم الأنواع والتعبيرات المستبدلة إلى تأثيرات جانبية مثل تأليف تخصصات قالب الفئة و / أو تخصصات قالب الوظيفة ، وتوليد وظائف محددة ضمنيًا ، إلخ. هذه الآثار الجانبية ليست في " السياق "ويمكن أن يؤدي إلى سوء تشكيل البرنامج. - note note ]

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

وبشكل أكثر تحديداً ، يمكن أن يكون الفرق بين وجود قالب قابل للاستخدام في SFINAE ، ونموذج غير ذلك .

سيلي مثال

template<typename SomeType>
struct inner_type { typedef typename SomeType::type type; };

template<
  class T,
  class   = typename T::type,            // (E)
  class U = typename inner_type<T>::type // (F)
> void foo (int);                        // preferred

template<class> void foo (...);          // fallback

struct A {                 };  
struct B { using type = A; };

int main () {
  foo<A> (0); // (G), should call "fallback "
  foo<B> (0); // (H), should call "preferred"
}

في السطر المكتوب عليه (G) نريد من المترجم أن يتحقق أولاً (E) وإذا نجح ذلك في تقييم (F) ، ولكن قبل التغيير القياسي الذي تمت مناقشته في هذا المنشور ، لم يكن هناك مثل هذا الضمان.


يشمل السياق المباشر للبدائل في foo(int) ؛

  • (E) التأكد من أن المار في T لديه ::type
  • (F) تأكد من أن inner_type<T> يحتوي على ::type


إذا تم تقييم (F) على الرغم من أن (E) ينتج عنه استبدال غير صالح ، أو إذا تم تقييم (F) قبل (E) لن يستخدم مثالنا القصير (السخيفة) SFINAE وسوف نحصل على قول تشخيصي بأن تطبيق غير مبرر .. على الرغم من أننا نعتزم استخدام foo(...) في مثل هذه الحالة.


ملاحظة: لاحظ أن SomeType::type غير موجود في السياق المباشر للقالب؛ يؤدي الفشل في typedef داخل inner_type إلى جعل التطبيق غير صحيح ومنع القالب من استخدام SFINAE .

ما الآثار المترتبة على ذلك في تطوير الكود في C ++ 14؟

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

وسيجعل أيضًا تبديل حجة القالب يتصرف بطريقة أكثر طبيعية للمحامين غير اللغويين ؛ وجود استبدال تحدث من اليسار إلى اليمين هو أكثر بديهية بكثير من erhm ، مثل ، أي وسيلة ، على مترجم ، أريد أن تفعل ، مثل erhm - ...

أليس هناك أي تأثير سلبي؟

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

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

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

القصة

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

تخيل أننا نعمل مع العدادين وأننا نرغب في طريقة للحصول بسهولة على القيمة الأساسية للتعداد المحدد.

نحن في الأساس مريضون ومتعبتون من الاضطرار دائمًا إلى الكتابة (A) ، عندما نرغب في شيء أقرب إلى (B) .

auto value = static_cast<std::underlying_type<EnumType>::type> (SOME_ENUM_VALUE); // (A)

auto value = underlying_value (SOME_ENUM_VALUE);                                  // (B)

التنفيذ الاصلي

قيل وفعلنا ، قررنا كتابة تنفيذ underlying_value بالنظر إلى ما يلي.

template<class T, class U = typename std::underlying_type<T>::type> 
U underlying_value (T enum_value) { return static_cast<U> (enum_value); }

هذا سوف يخفف من آلامنا ، ويبدو أنه يفعل بالضبط ما نريد ؛ نمر في العداد ، ونحصل على القيمة الأساسية مرة أخرى.

نقول لأنفسنا أن هذا التنفيذ رائع ويطلب من زميلنا ( دون كيشوت ) الجلوس ومراجعة تنفيذنا قبل دفعه إلى الإنتاج.

استعراض الكود

Don Quixote هو مطور C ++ ذو خبرة يحتوي على كوب من القهوة في يد واحدة ، ومعيار C ++ في الآخر. من الغموض كيف تمكن من كتابة سطر واحد من الكود بكلا اليدين مشغول ، ولكن هذه قصة مختلفة.

ويستعرض الكود الخاص بنا ويخلص إلى الاستنتاج القائل بأن التنفيذ غير آمن ، فنحن بحاجة إلى حماية std::underlying_type من سلوك غير محدد لأنه يمكننا المرور في T ليس من نوع العد .

20.10.7.6 - تحولات أخرى - [meta.trans.other]

template<class T> struct underlying_type;

الحالة: يجب أن يكون T نوع تعداد (7.2)
التعليقات: يجب أن يقوم type typedef العضو بتسمية النوع الأساسي لـ T

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

ليلة البارحة

لا يصرخ شيئا عن الكيفية التي ينبغي لنا دائما أن نحترم معايير C ++ ، وأننا يجب أن نشعر بخزي عظيم لما فعلناه .. إنه أمر غير مقبول.

بعد أن يهدأ ، وكان لديه رشفات أخرى من القهوة ، يقترح أن نغير التنفيذ لإضافة حماية ضد std::underlying_type مع شيء غير مسموح به.

template<
  typename T,
  typename   = typename std::enable_if<std::is_enum<T>::value>::type,  // (C)
  typename U = typename std::underlying_type<T>::type                  // (D)
>
U underlying_value (T value) { return static_cast<U> (value); }

ال WINDMILL

نشكر Don على اكتشافاته ونشعر الآن بالرضا عن تطبيقنا ، ولكن فقط حتى ندرك أن ترتيب استبدال وسيطة القالب ليس محددًا بشكل جيد في C ++ 11 (ولا يتم ذكره عند توقف الاستبدال).

برمجيًا مثل C ++ 11 ، يمكن أن يتسبب التنفيذ في إنشاء مثيل لـ std::underlying_type مع T ليس من نوع التعداد بسبب سببين:

  1. المترجم مجاني لتقييم (D) قبل (C) لأن ترتيب الاستبدال غير محدد بشكل جيد ، و ؛

  2. حتى إذا كان المترجم يقوم بتقييم (C) قبل (D) ، فإنه ليس مضمونًا أنه لن يتم تقييمه (D) ، C ++ 11 لا يحتوي على جملة تقول صراحة عندما يجب أن تتوقف سلسلة الاستبدال.

سيكون تطبيق دون خالي من السلوك غير المحدد في C ++ 14 ، ولكن فقط لأن C ++ 14 ينص صراحة على أن الاستبدال سيستمر في ترتيب معجمي ، وأنه سيتوقف كلما تسبب الاستبدال في الفشل .

قد لا يكون دون قتال طواحين الهواء على هذا واحد ، لكنه بالتأكيد غاب عن تنين مهم جدا في معيار C ++ 11.

يتطلب التنفيذ الصحيح في C ++ 11 التأكد من أنه بغض النظر عن الترتيب الذي يحدث به استبدال معلمات القالب لن تكون لحظة std::underlying_type مع نوع غير صالح.

#include <type_traits>

namespace impl {
  template<bool B, typename T>
  struct underlying_type { };

  template<typename T>
  struct underlying_type<true, T>
    : std::underlying_type<T>
  { };
}

template<typename T>
struct underlying_type_if_enum
  : impl::underlying_type<std::is_enum<T>::value, T>
{ };

template<typename T, typename U = typename underlying_type_if_enum<T>::type>
U get_underlying_value (T value) {
  return static_cast<U> (value);  
}

ملاحظة: تم استخدام الملف basic_type لأنه طريقة بسيطة لاستخدام شيء ما في المعيار ضد ما هو موجود في المعيار ؛ الشيء المهم هو أن instantiating مع غير enum هو سلوك غير معرف .

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

C ++ 11

14.8.2 - خصم الوسيطة [temp.deduct] - [temp.deduct]

7 يحدث الاستبدال في جميع الأنواع والتعبيرات المستخدمة في نوع الوظيفة وفي إعلانات معلمات القالب. تتضمن التعبيرات ليس فقط تعبيرات ثابتة مثل تلك التي تظهر في حدود الصفيف أو كوسيط قالب nontype ولكن أيضًا التعبيرات العامة (مثل التعبيرات غير الثابتة) داخل decltype ، decltype ، وسياقات أخرى تسمح بالتعبيرات غير الثابتة.

C ++ 14

14.8.2 - خصم الوسيطة [temp.deduct] - [temp.deduct]

7 يحدث الاستبدال في جميع الأنواع والتعبيرات المستخدمة في نوع الوظيفة وفي إعلانات معلمات القالب. تتضمن التعبيرات ليس فقط تعبيرات ثابتة مثل تلك التي تظهر في حدود الصفيف أو كوسيط قالب nontype ولكن أيضًا التعبيرات العامة (مثل التعبيرات غير الثابتة) داخل decltype ، decltype ، وسياقات أخرى تسمح بالتعبيرات غير الثابتة. ويستمر الاستبدال بترتيب معجمي ويتوقف عند مواجهة شرط يؤدي إلى الفشل .

تنص الجملة المضافة صراحةً على ترتيب الاستبدال عند التعامل مع معلمات القالب في C ++ 14.

ترتيب الاستبدال أمر لا يُعطى في الغالب الكثير من الاهتمام. لم أجد حتى الآن ورقة واحدة عن سبب أهمية ذلك. ربما يكون ذلك لأن C ++ 1y لم يتم توحيدها بالكامل بعد ، لكنني أفترض أن مثل هذا التغيير يجب أن يكون قد تم تقديمه لسبب ما.

السؤال:

  • لماذا ، ومتى ، هل ترتيب استبدال وسيطة القالب مهم؟




c++14