c++ - لماذا تستخدم التعبيرات الثابتة استبعادًا لسلوك غير معروف؟



c++11 undefined-behavior sfinae constexpr (4)

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

لقد كنت أبحث في ما هو مسموح به في تعبير ثابت دائم * ، والذي تمت تغطيته في القسم 5.19 تعبيرات ثابتة الفقرة 2 من مسودة معيار C ++ والتي تقول:

التعبير الشرطي هو عبارة عن تعبير ثابت أساسي ما لم يتضمن أحد ما يلي كإيضاح جغرافي محتمل (3.2) ، ولكن يتم التعبير عن subexpressions من المنطقي AND (5.14) ، منطقية OR (5.15) ، وعمليات (5.16) مشروطة لم يتم تقييمها لا تعتبر [ملحوظة: يستدعي عامل فوق طاقته دالة. - ملاحظة نهاية]:

ويسرد الاستثناءات في التعداد النقطي التالي ويتضمن ( التركيز الألغام ):

- عملية من شأنها أن يكون لها سلوك غير محدد [ملاحظة: بما في ذلك ، على سبيل المثال ، تجاوز عدد صحيح صحيح (البند 5) ، حساب مؤشر معين (5.7) ، القسمة على صفر (5.6) ، أو بعض عمليات التحول (5.8) - تدوين ملاحظة) ؛

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

هل توفر لنا هذه الفقرة أية مزايا أو أدوات لن يكون لدينا بدونها؟

كمرجع ، يبدو هذا بمثابة المراجعة الأخيرة للاقتراح الخاص بالتعبيرات الثابتة المعممة .


عندما نتحدث عن سلوك غير معروف ، من المهم أن نتذكر أن Standard يترك السلوك غير محدد لهذه الحالات. لا يحظر عمليات التنفيذ من تقديم ضمانات أقوى. على سبيل المثال ، قد تضمن بعض التطبيقات أن التدفق الإضافي الموقّع يلتف حول ، في حين أن البعض الآخر قد يضمن التشبع.

سيؤدي إلزام المجمعين بمعالجة التعبيرات الثابتة التي تتضمن سلوكًا غير محدد إلى تقييد الضمانات التي يمكن أن يوفرها تنفيذ ما ، مما يقيدهم على إنتاج بعض القيمة بدون آثار جانبية (ما يطلق عليه Standard القيمة غير المحددة ). يستثني ذلك الكثير من الضمانات الممتدة الموجودة في العالم الحقيقي.

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

لذلك ، يتم رفض هذه التعبيرات في وقت الترجمة لتجنب ضياع الآثار الجانبية في بيئة التنفيذ.


الصياغة هي في الواقع موضوع تقرير الخلل رقم 1313 الذي يقول:

لا تتطلب متطلبات التعبيرات الثابتة حاليًا ، ولكن يجب ، استبعاد التعبيرات التي لها سلوك غير معرف ، مثل مؤشر المؤشر عندما لا تشير المؤشرات إلى عناصر من نفس الصفيف.

القرار هو الصيغة الحالية التي لدينا الآن ، لذا كان المقصود بذلك بوضوح ، فما هي الأدوات التي يوفرها لنا هذا؟

دعونا نرى ما يحدث عندما نحاول إنشاء متغير constexpr مع تعبير يحتوي على سلوك غير معرف ، سوف نستخدم clang لكل الأمثلة التالية. هذا الرمز ( انظر على الهواء ):

constexpr int x = std::numeric_limits<int>::max() + 1 ;

ينتج الخطأ التالي:

error: constexpr variable 'x' must be initialized by a constant expression
    constexpr int x = std::numeric_limits<int>::max() + 1 ;
                  ^   ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
note: value 2147483648 is outside the range of representable values of type 'int'
    constexpr int x = std::numeric_limits<int>::max() + 1 ;
                                       ^

هذا الرمز ( انظر على الهواء ):

constexpr int x = 1 << 33 ;  // Assuming 32-bit int

ينتج هذا الخطأ:

error: constexpr variable 'x' must be initialized by a constant expression
    constexpr int x = 1 << 33 ;  // Assuming 32-bit int
             ^   ~~~~~~~
note: shift count 33 >= width of type 'int' (32 bits)
    constexpr int x = 1 << 33 ;  // Assuming 32-bit int
                  ^

وهذا الرمز الذي لديه سلوك غير معروف في وظيفة constexpr:

constexpr const char *str = "Hello World" ;      

constexpr char access( int index )
{
    return str[index] ;
}

int main()
{
    constexpr char ch = access( 20 ) ;
}

ينتج هذا الخطأ:

error: constexpr variable 'ch' must be initialized by a constant expression
    constexpr char ch = access( 20 ) ;
                   ^    ~~~~~~~~~~~~

 note: cannot refer to element 20 of array of 12 elements in a constant expression
    return str[index] ;
           ^

حسنا هذا مفيد يمكن للمجمع أن يكشف عن سلوك غير محدد في constexpr ، أو على الأقل ما يعتقده clang غير محدد . لاحظ أن gcc يتصرف بنفس الطريقة إلا في حالة السلوك غير المعرّف بالانتقال إلى اليسار واليمين ، فعادة ما ينتج gcc تحذيرًا في هذه الحالات ولكن لا يزال يرى التعبير ثابتًا.

يمكننا استخدام هذه الوظيفة عبر SFINAE لاكتشاف ما إذا كان تعبير الإضافة قد يتسبب في تجاوز السعة أم لا ، وقد استوحى المثال المفتعل التالي من إجابة dyp الذكية هنا :

#include <iostream>
#include <limits>

template <typename T1, typename T2>
struct addIsDefined
{
     template <T1 t1, T2 t2>
     static constexpr bool isDefined()
     {
         return isDefinedHelper<t1,t2>(0) ;
     }

     template <T1 t1, T2 t2, decltype( t1 + t2 ) result = t1+t2>
     static constexpr bool isDefinedHelper(int)
     {
         return true ;
     }

     template <T1 t1, T2 t2>
     static constexpr bool isDefinedHelper(...)
     {
         return false ;
     }
};


int main()
{    
    std::cout << std::boolalpha <<
      addIsDefined<int,int>::isDefined<10,10>() << std::endl ;
    std::cout << std::boolalpha <<
     addIsDefined<int,int>::isDefined<std::numeric_limits<int>::max(),1>() << std::endl ;
    std::cout << std::boolalpha <<
      addIsDefined<unsigned int,unsigned int>::isDefined<std::numeric_limits<unsigned int>::max(),std::numeric_limits<unsigned int>::max()>() << std::endl ;
}

مما يؤدي إلى ( رؤيته على الهواء ):

true
false
true

ليس من الواضح أن المعيار يتطلب هذا السلوك ولكن على ما يبدو هذا التعليق من قبل هوارد هينانت يشير إلى أنه في الواقع:

[...] وهو أيضًا constexpr ، بمعنى أنه يتم ضبط UB في وقت التجميع

تحديث

بطريقة ما فاتني مشكلة 695 أخطاء حساب وقت التحويل البرمجي في وظائف constexpr التي تدور حول صياغة الفقرة 4 من القسم 5 والتي اعتادت أن تقول ( التأكيد على الألغام في المستقبل ):

إذا لم يتم تعريف النتيجة رياضياً خلال تقييم تعبير ، أو لا في نطاق القيم التي يمكن تمثيلها لنوعها ، فإن السلوك غير معروف ، ما لم يظهر هذا التعبير في حالة الحاجة إلى تعبير ثابت لا يتجزأ (5.19 [expr.const] ) ، وفي هذه الحالة يكون البرنامج غير سليم .

ويذهب ليقول:

مقصودًا كالتقليد الاعتيادي المقبول لـ "تم تقييمه في وقت التجميع" ، وهو مفهوم لا يحدده المعيار بشكل مباشر. ليس من الواضح أن هذه الصيغة تغطي بشكل كاف وظائف constexpr.

و ملاحظة لاحقة تقول:

[...] هناك توتر بين الرغبة في تشخيص الأخطاء في وقت التجميع مقابل عدم تشخيص الأخطاء التي لن تحدث في وقت التشغيل فعليًا. [...] كان إجماع مجموعة CWG هو أن التعبير مثل 1/0 يجب أن يكون ببساطة تعتبر غير ثابتة أي تشخيص قد ينتج عن استخدام التعبير في سياق يتطلب تعبيرًا ثابتًا.

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

لا يمكننا القول بالتأكيد كان هذا هو القصد ولكن يقترح أنه كان بقوة. الفرق في كيفية التعامل مع تغيرات clang و gcc غير محددة لا يترك مجالا للشك.

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


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

f (a,b)

سابقا إما ثم ب ، أو ، ب ثم أ. الآن ، يمكن تقييم a و b مع التعليمات المشذرة أو حتى في النوى المختلفة.





c++ c++11 undefined-behavior sfinae constexpr