المصطلح
"سلوك غير محدد" في لغة C و C ++ يعين موقفًا فيه حرفيًا "ما لا يحدث". تاريخيا ، كانت الحالات التي تصرف فيها المترجمون السابقون لـ C (والمعماريات الموجودة عليها) بطريقة غير متوافقة تنسب إلى السلوك غير المحدد ، وقررت لجنة تطوير المعيار ، بحكمته غير المحدودة ، عدم اتخاذ قرار بشأن هذا (أي عدم إعطاء الأفضلية بعض من التطبيقات المتنافسة). كان يُطلق على السلوك غير المحدد المواقف المحتملة التي لا يصف فيها المعيار ، عادةً ما يكون شاملاً للغاية ، أي سلوك محدد. هذا المصطلح له معنى ثالث ، والذي أصبح في عصرنا أكثر وأكثر صلة: سلوك غير محدد - هذه هي فرصة التحسين. والمطورين في C و C ++
يحبون التحسينات ؛ أنها تتطلب بإصرار المجمعين لبذل كل جهد ممكن لتسريع التعليمات البرمجية.
تم نشر هذا المقال لأول مرة على موقع خدمات التشفير. يتم نشر الترجمة بإذن من المؤلف توماس Pornin.هنا مثال كلاسيكي:
void foo(double *src, int *dst) { int i; for (i = 0; i < 4; i ++) { dst[i] = (int)src[i]; } }
سنقوم بتجميع رمز دول مجلس التعاون الخليجي هذا على نظام تشغيل x86 64 بت لنظام التشغيل Linux (أعمل على أحدث إصدار من Ubuntu 18.04 ، إصدار GCC - 7.3.0). قم بتشغيل التحسين الكامل ، ثم انظر إلى قائمة المجمّع ، حيث نستخدم المفاتيح
"-W -Wall -O9 -S " (الوسيطة "
-O9 " تعيّن الحد الأقصى لمستوى التحسين في دول مجلس التعاون الخليجي ، والذي في الممارسة العملية يعادل "
-O3 " ، على الرغم من أنه في بعض الشوكات دول مجلس التعاون الخليجي محددة ومستويات أعلى). نحصل على النتيجة التالية:
.file "zap.c" .text .p2align 4,,15 .globl foo .type foo, @function foo: .LFB0: .cfi_startproc movupd (%rdi), %xmm0 movupd 16(%rdi), %xmm1 cvttpd2dq %xmm0, %xmm0 cvttpd2dq %xmm1, %xmm1 punpcklqdq %xmm1, %xmm0 movups %xmm0, (%rsi) ret .cfi_endproc .LFE0: .size foo, .-foo .ident "GCC: (Ubuntu 7.3.0-27ubuntu1~18.04) 7.3.0" .section .note.GNU-stack,"",@progbits
ينقل كل من إرشادات
movupd الأولى والثانية
قيمتين مزدوجتين إلى سجل SSE2 ذي 128 بت (حجم
مزدوج يبلغ 64 بت ، بحيث يمكن لسجل SSE2 تخزين قيمتين
مزدوجتين ). بمعنى آخر ، تتم قراءة أربع قيم أولية أولاً ، وعندها فقط يتم
تحويلها إلى
int (عملية
cvttpd2dq ). تقوم عملية
punpcklqdq بنقل الأعداد الصحيحة 32 بت الأربعة المستلمة في سجل SSE2 واحد
(٪ xmm0 ) ، ثم تتم كتابة محتوياته على ذاكرة الوصول العشوائي (
movups ). والآن الشيء الرئيسي: يتطلب برنامج C- رسميًا أن يحدث الوصول إلى الذاكرة بالترتيب التالي:
- قراءة القيمة المزدوجة الأولى من src [0] .
- اكتب القيمة الأولى من النوع int إلى dst [0] .
- قراءة القيمة المزدوجة الثانية من src [1] .
- اكتب القيمة الثانية للنوع int إلى dst [1] .
- قراءة القيمة المزدوجة الثالثة من src [2] .
- اكتب القيمة الثالثة من النوع int إلى dst [2] .
- قراءة القيمة المزدوجة الرابعة من src [3] .
- اكتب القيمة الرابعة للنوع int إلى dst [3] .
ومع ذلك ، فإن كل هذه المتطلبات لا معنى لها إلا في سياق آلة تجريدية ، والتي يحددها المعيار C ؛ قد يختلف الإجراء على جهاز حقيقي. المترجم حر في إعادة ترتيب العمليات أو تعديلها ، بشرط ألا تتعارض نتيجتها مع دلالات الجهاز التجريدي (ما يسمى
بـ "كما لو" هي "كما لو"). في مثالنا ، ترتيب العمل مختلف تمامًا:
- قراءة القيمة المزدوجة الأولى من src [0] .
- قراءة القيمة المزدوجة الثانية من src [1] .
- قراءة القيمة المزدوجة الثالثة من src [2] .
- قراءة القيمة المزدوجة الرابعة من src [3] .
- اكتب القيمة الأولى من النوع int إلى dst [0] .
- اكتب القيمة الثانية للنوع int إلى dst [1] .
- اكتب القيمة الثالثة من النوع int إلى dst [2] .
- اكتب القيمة الرابعة للنوع int إلى dst [3] .
هذه هي اللغة C: جميع محتويات الذاكرة هي في النهاية بايت (أي فتحات مع قيم type
char غير موقعة ، ولكن في الممارسة العملية ، يُسمح بمجموعات من ثمانية بتات) ، وأي عمليات مؤشر تعسفي مسموح بها. على وجه الخصوص ، يمكن استخدام مؤشرات
src و
dst للوصول إلى أجزاء متراكبة من الذاكرة عند الاتصال (تسمى هذه الحالة "التعرجات"). وبالتالي ، قد يكون ترتيب القراءة والكتابة مهمًا إذا تم كتابة وحدات البايت ثم قراءتها مرة أخرى. لكي يتوافق السلوك الفعلي للبرنامج مع الملخص المعرف بواسطة المعيار C ، سيتعين على المحول البرمجي أن يتناوب بين عمليات القراءة والكتابة ، مما يوفر دورة كاملة من الوصول إلى الذاكرة عند كل تكرار. سيكون رمز الناتجة أكبر وسيعمل أبطأ بكثير. للمطورين C ، سيكون هذا حزن.
هنا ، لحسن الحظ ، يأتي
السلوك غير المحدد للإنقاذ. ينص المعيار C على أنه لا يمكن الوصول إلى القيم من خلال مؤشرات لا يتوافق نوعها مع الأنواع الحالية من هذه القيم. ببساطة ، إذا كانت القيمة مكتوبة على
dst [0] ، حيث
dst هي مؤشر
int ، فلا يمكن قراءة البايتات المقابلة عبر
src [1] ، حيث
src عبارة عن مؤشر
مزدوج ، لأننا في هذه الحالة سنحاول الوصول قيمة ، والتي هي الآن من النوع
int ، باستخدام مؤشر من نوع غير متوافق. في هذه الحالة ، سيحدث سلوك غير محدد. جاء ذلك في الفقرة 7 من القسم 6.5 من المواصفة القياسية ISO 9899: 1999 ("C99") (في الإصدار الجديد 9899: 2018 ، أو "C17" ، الصيغة لم تتغير). يسمى هذا الشرط القاعدة التعرج صارمة. نتيجة لذلك ، يُسمح للمترجم C بالتصرف على افتراض أن عمليات الوصول إلى الذاكرة التي تؤدي إلى سلوك غير محدد بسبب انتهاك قاعدة الاسم المستعار الصارمة لا تحدث. وبالتالي ، يمكن للمترجم إعادة ترتيب عمليات القراءة والكتابة بأي ترتيب ، حيث لا ينبغي لها الوصول إلى أجزاء متراكبة من الذاكرة. هذا هو ما رمز التحسين هو كل شيء.
باختصار ، معنى السلوك غير المحدد هو: يمكن للمترجم أن يفترض أنه لن يكون هناك أي سلوك غير محدد ، ويقوم بإنشاء كود يعتمد على هذا الافتراض. في حالة قاعدة التعرجات الصارمة - بشرط أن يحدث التعرج ، يسمح السلوك غير المحدود بالتحسينات المهمة التي يصعب تنفيذها. بشكل عام ، كل تعليمات في إجراءات إنشاء التعليمات البرمجية المستخدمة من قبل برنامج التحويل البرمجي لها تبعيات تقيد خوارزمية تخطيط العملية: لا يمكن تنفيذ التعليمات قبل التعليمات التي تعتمد عليها ، أو بعد تلك التعليمات التي تعتمد عليها. في مثالنا ، يحل السلوك غير المحدد التبعيات بين عمليات الكتابة في
dst [] وعمليات القراءة "اللاحقة" من
src [] : يمكن أن يوجد مثل هذا الاعتماد فقط في الحالات التي يحدث فيها سلوك غير محدد عند الوصول إلى الذاكرة. وبالمثل ، يسمح مفهوم السلوك غير المحدد للمترجم بإزالة التعليمة البرمجية التي لا يمكن تنفيذها دون إدخال حالة من السلوك غير المحدد.
كل هذا ، بالطبع ، جيد ، لكن هذا السلوك يُنظر إليه أحيانًا على أنه خيانة غادرة من قبل المترجم. يمكنك سماع العبارة غالبًا: "يستخدم المترجم مفهوم السلوك غير المحدد كذريعة لكسر الكود." لنفترض أن شخصًا ما يكتب برنامجًا يضيف أعدادًا صحيحة ويخشى تجاوز السعة - تذكر
حالة البيتكوين . يمكنه التفكير في هذا: لتمثيل أعداد صحيحة ، يستخدم المعالج رمزًا إضافيًا ، مما يعني أنه إذا حدث تجاوز سعة ، فسيحدث ذلك لأن النتيجة سيتم اقتطاعها إلى حجم النوع ، أي 32 بت هذا يعني أن نتيجة الفائض يمكن التنبؤ بها والتحقق منها من خلال اختبار.
سيقوم مطورنا الشرطي بكتابة هذا:
#include <stdio.h> #include <stdlib.h> int add(int x, int y, int *z) { int r = x + y; if (x > 0 && y > 0 && r < x) { return 0; } if (x < 0 && y < 0 && r > x) { return 0; } *z = r; return 1; } int main(int argc, char *argv[]) { int x, y, z; if (argc != 3) { return EXIT_FAILURE; } x = atoi(argv[1]); y = atoi(argv[2]); if (add(x, y, &z)) { printf("%d\n", z); } else { printf("overflow!\n"); } return 0; }
الآن دعونا نحاول ترجمة هذا الرمز باستخدام GCC:
$ gcc -W -Wall -O9 testadd.c $ ./a.out 17 42 59 $ ./a.out 2000000000 1500000000 overflow!
حسنا ، يبدو أن العمل. جرب الآن مترجمًا آخر ، على سبيل المثال ، Clang (لدي الإصدار 6.0.0):
$ clang -W -Wall -O3 testadd.c $ ./a.out 17 42 59 $ ./a.out 2000000000 1500000000 -794967296
ماذا؟
اتضح أنه عندما تؤدي العملية ذات أنواع الأعداد الصحيحة الموقعة إلى نتيجة لا يمكن تمثيلها بالنوع المستهدف ، فإننا ندخل منطقة السلوك غير المحدد. ولكن قد يفترض المترجم أنه لا يحدث. على وجه الخصوص ، تحسين التعبير
x> 0 && y> 0 && r <x ، يخلص المترجم إلى أنه نظرًا لأن قيمتي
x و
y موجبتان تمامًا ، فلن يكون التحقق الثالث صحيحًا (مجموع القيمتين لا يمكن أن يكونا أقل من أي منهما) ، ويمكنك تخطي هذه العملية برمتها. بمعنى آخر ، نظرًا لأن التجاوز هو سلوك غير محدد ، فإنه "لا يمكن أن يحدث" من وجهة نظر المترجم ، ويمكن حذف جميع الإرشادات التي تعتمد على هذه الحالة. اختفت آلية اكتشاف السلوك غير المحدد.
لم يشر المعيار مطلقًا إلى أن "دلالات موقعة" (والتي تُستخدم فعليًا في عمليات المعالج) تُستخدم في العمليات الحسابية ذات الأنواع الموقعة ؛ حدث هذا بالأحرى عن طريق التقاليد - حتى في تلك الأيام التي لم يكن فيها المترجمون أذكياء بدرجة كافية لتحسين الكود ، مع التركيز على مجموعة من القيم. يمكنك فرض Clang و GCC لتطبيق دلالات التفاف على أنواع موقعة باستخدام العلامة الخاصة
-wwrapv (في Microsoft Visual C ، يمكنك استخدام
-d2UndefIntOverflow- ، كما هو موضح
هنا ). ومع ذلك ، لا يمكن الاعتماد على هذا النهج ، فقد تختفي العلامة عند نقل الرمز إلى مشروع آخر أو إلى بنية أخرى.
قليل من الناس يعرفون أن تجاوزات نوع الحرف تنطوي على سلوك غير محدد. جاء ذلك في الفقرة 5 من القسم 6.5 من المعيارين C99 و C17:
في حالة حدوث استثناء عند تقييم تعبير (على سبيل المثال ، إذا لم تكن النتيجة محددة رياضيا أو كانت خارج نطاق القيم الصالحة لنوع معين) ، فإن السلوك غير معرف.بالنسبة للأنواع غير الموقعة ، ومع ذلك ، فإن الدلالات المعيارية مضمونة. تقول الفقرة 9 من القسم 6.2.5 ما يلي:
لا يحدث تجاوز السعة أبدًا في العمليات الحسابية باستخدام المعاملات غير الموقعة ، حيث إن النتيجة التي لا يمكن تمثيلها بنوع الأعداد الصحيحة غير الموقعة الناتجة يتم اقتطاعها بمعامل رقم يمثل أكثر من الحد الأقصى للقيمة الممثلة بالنوع الناتج.مثال آخر على السلوك غير المحدد في العمليات ذات الأنواع الموقعة هو عملية القسمة. كما يعلم الجميع ، لا يتم تحديد نتيجة القسمة على صفر حسابيًا ، وبالتالي ، وفقًا للمعيار ، تستلزم هذه العملية سلوكًا غير محدد. إذا كان المقسم صفراً في عملية
idiv على معالج x86 ، يتم طرح استثناء للمعالج. مثل طلبات المقاطعة ، تتم معالجة استثناءات المعالج بواسطة نظام التشغيل. في الأنظمة المشابهة لـ Unix ، مثل Linux ، يتم ترجمة استثناء المعالج الذي يتم تشغيله بواسطة عملية
idiv إلى إشارة
SIGFPE ، والتي يتم إرسالها إلى العملية ، وتنتهي
بالمعالج الافتراضي (لا تتفاجأ بأن "FPE" تعني "استثناء
الفاصلة العائمة" (استثناء في عمليات
الفاصلة العائمة) ، بينما يعمل
idiv مع الأعداد الصحيحة). ولكن هناك موقف آخر يؤدي إلى سلوك غير محدد. النظر في التعليمات البرمجية التالية:
#include <stdio.h> #include <stdlib.h> int main(int argc, char *argv[]) { int x, y; if (argc != 3) { return EXIT_FAILURE; } x = atoi(argv[1]); y = atoi(argv[2]); printf("%d\n", x / y); return 0; } : $ gcc -W -Wall -O testdiv.c $ ./a.out 42 17 2 $ ./a.out -2147483648 -1 zsh: floating point exception (core dumped) ./a.out -2147483648 -1
والحقيقة هي: على هذا الجهاز (نفس x86 لنظام التشغيل Linux) ، يمثل النوع
int مجموعة من القيم من -2،147،483،648 إلى +2،147،483،647. إذا قسمت -2،147،483،648 على -1 ، يجب أن تحصل على لكن هذا الرقم ليس في نطاق القيم
int . لذلك ، لم يتم تعريف السلوك. أي شيء يمكن أن يحدث. في هذه الحالة ، يتم إنهاء العملية بالقوة. في نظام آخر ، خاصة مع معالج صغير لا يحتوي على عملية تقسيم ، قد تختلف النتيجة. في مثل هذه البنى ، يتم تنفيذ التقسيم برمجيًا - بمساعدة الإجراء الذي يتم توفيره عادةً بواسطة المحول البرمجي ، والآن يمكنه القيام بكل ما يشاء بسلوك غير محدد ، لأن هذا هو بالضبط ما هو عليه.
ألاحظ أنه يمكن الحصول على
SIGFPE في نفس الظروف وبمساعدة مشغل المعامل (
٪ ). في الواقع: تحتها تكمن نفس العملية
idiv ، التي تحسب كل من الباقي والباقي ، لذلك يتم تشغيل نفس استثناء المعالج. ومن المثير للاهتمام ، أن معيار C99 يقول أن تعبير
INT_MIN٪ -1 لا يمكن أن يؤدي إلى سلوك غير محدد ، لأن النتيجة محددة رياضياً (صفر) وتدخل بوضوح نطاق قيم النوع المستهدف. في الإصدار C17 ، تم تغيير نص الفقرة 6 من القسم 6.5.5 ، والآن يتم أخذ هذه الحالة في الاعتبار أيضًا ، مما يجعل المعيار أقرب إلى الوضع الحقيقي على منصات الأجهزة الشائعة.
هناك العديد من المواقف غير الواضحة التي تؤدي أيضًا إلى سلوك غير محدد. ألقِ نظرة على هذا الكود:
#include <stdio.h> #include <stdlib.h> unsigned short mul(unsigned short x, unsigned short y) { return x * y; } int main(int argc, char *argv[]) { int x, y; if (argc != 3) { return EXIT_FAILURE; } x = atoi(argv[1]); y = atoi(argv[2]); printf("%d\n", mul(x, y)); return 0; }
هل تعتقد أن البرنامج ، وفقًا لمعيار C ، يجب أن يطبع إذا مررنا العوامل 45000 و 50000 إلى الوظيفة؟
- 18048
- 2،250،000،000
- حفظ الله الملكة!
الجواب الصحيح ... نعم ، كل ما سبق! ربما تكون قد جادلت على هذا النحو: نظرًا لأن
الاختصار غير الموقَّع هو نوع غير موقَّع ، يجب أن يدعم دلالات الالتفاف modulo 65 536 ، نظرًا لأن حجم هذا النوع في المعالج x86 ، كقاعدة عامة ، هو 16 بت تمامًا (يسمح المعيار أيضًا بحجم أكبر ، ولكن في الممارسة العملية ، هذا لا يزال نوع 16 بت). نظرًا لأن المنتج يبلغ 2،250،000،000 رياضيًا ، فسيتم اقتطاعه من المعامل 65،536 ، والذي يقدم إجابة قدرها 18،048. ومع ذلك ، وبالتفكير بهذه الطريقة ، ننسى امتداد أنواع الأعداد الصحيحة. وفقًا للمعيار C (القسم 6.3.1.1 ، الفقرة 2) ، إذا كانت المعاملات من نوع يكون حجمه أصغر تمامًا من حجم
int ، ويمكن تمثيل قيم هذا النوع بالنوع
int بدون فقد البتات (ولدينا هذه الحالة فقط: على x86 الخاصة بي تحت يصل حجم Linux إلى 32 بت ، ويمكنه تخزين القيم من 0 إلى 65.535 بشكل صريح ، ثم يتم تحويل كلا المعاملين إلى
int ويتم تنفيذ العملية بالفعل على القيم المحولة. وهي: يتم احتساب المنتج كقيمة type
int ، وفقط عند الرجوع من الوظيفة يتم
إرجاعه إلى
اختصار غير موقَّع (أي أنه في هذه اللحظة يحدث اقتطاع modulo 65 536). المشكلة هي أن النتيجة قبل التحويل العكسي هي 2.250 مليون ، وهذه القيمة تتجاوز نطاق
int ، وهو نوع موقَّع. نتيجة لذلك ، نحصل على سلوك غير محدد. بعد ذلك ، يمكن أن يحدث أي شيء ، بما في ذلك نوبات مفاجئة من الوطنية الإنجليزية.
ومع ذلك ، في الممارسة العملية ، مع المجمعين العاديين ، فإن النتيجة هي 18048 ، حيث لا يوجد حتى الآن الأمثل الذي يمكن الاستفادة من السلوك غير المحدد في هذا البرنامج بالذات (يمكن للمرء أن يتخيل المزيد من السيناريوهات المصطنعة حيث يمكن أن يسبب المتاعب حقا).
أخيرًا ، مثال آخر ، الآن في C ++:
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <array> int main(int argc, char *argv[]) { std::array<char, 16> tmp; int i; if (argc < 2) { return EXIT_FAILURE; } memset(tmp.data(), 0, 16); if (strlen(argv[1]) < 16) { strcpy(tmp.data(), argv[1]); } for (i = 0; i < 17; i ++) { printf(" %02x", tmp[i]); } printf("\n"); }
هذا ليس "
strcpy سيئة النكراء
() سيئة" لك. في الواقع ، هنا يتم تنفيذ الدالة
strcpy () فقط إذا كان حجم السلسلة المصدر ، بما في ذلك محطة الصفر ، صغيرًا بدرجة كافية. علاوة على ذلك ، تتم تهيئة عناصر الصفيف بشكل صريح إلى صفر ، بحيث يكون لجميع وحدات البايت في الصفيف قيمة معينة ، بغض النظر عما إذا تم تمرير سلسلة كبيرة أو صغيرة إلى الدالة. في نفس الوقت ، فإن الحلقة في النهاية غير صحيحة: فهي تقرأ بايت واحد أكثر مما ينبغي.
قم بتشغيل الكود:
$ g++ -W -Wall -O9 testvec.c $ ./a.out foo 66 6f 6f 00 00 00 00 00 00 00 00 00 00 00 00 00 10 58 ffffffca ff ffffac ffffffc0 55 00 00 00 ffffff80 71 34 ffffff99 07 ffffffba ff ffffea ffffffd0 ffffffe5 44 ffffff83 fffffffd 7f 00 00 00 00 00 00 00 00 00 00 10 58 ffffffca ffffffac ffffffc0 55 00 00 ffffff97 7b 12 1b ffffffa1 7f 00 00 02 00 00 00 00 00 00 00 ffffffd8 ffffffe5 44 ffffff83 fffffffd 7f 00 00 00 ffffff80 00 00 02 00 00 00 60 56 (...) 62 64 3d 30 30 zsh: segmentation fault (core dumped) ./a.out foo ++?
يمكنك الاعتراض بسذاجة: حسنًا ، يقرأ بايتًا إضافيًا خارج حدود المصفوفة ؛ لكن هذا ليس مخيفًا للغاية ، لأنه في المجموعة لا تزال هذه البايتة موجودة ، يتم تعيينها إلى الذاكرة ، وبالتالي فإن المشكلة الوحيدة هنا هي العنصر السابع عشر الإضافي ذو القيمة غير المعروفة. ستظل الدورة تطبع 17 أعداد صحيحة تمامًا (بالتنسيق الست عشري) وتنتهي بدون أي شكاوى.
لكن المترجم لديه رأيه الخاص في هذا الشأن. إنه يدرك جيدًا أن القراءة السابعة عشرة تثير سلوكًا غير محدد. وفقًا لمنطقه ، فإن أي تعليمات لاحقة تكون في حالة غموض: لا يوجد أي شرط أنه بعد السلوك غير المحدد يجب أن يكون هناك شيء على الإطلاق (رسميًا ، حتى التعليمات السابقة قد تتعرض للهجوم ، لأن السلوك غير المحدد يعمل أيضًا في الاتجاه المعاكس). في حالتنا ، سيتجاهل المترجم ببساطة فحص الحالة في الحلقة ، وسوف يدور إلى الأبد ، أو بالأحرى ، حتى يبدأ في القراءة خارج الذاكرة المخصصة
للمكدس ، وبعد ذلك
ستعمل إشارة
SIGSEGV .
إنه أمر مضحك ، لكن إذا بدأت دول مجلس التعاون الخليجي بإعدادات أقل عدوانية للتحسينات ، فستصدر تحذيرًا:
$ g++ -W -Wall -O1 testvec.c testvec.c: In function 'int main(int, char**)': testvec.c:20:15: warning: iteration 16 invokes undefined behavior [-Waggressive-loop-optimizations] printf(" %02x", tmp[i]); ~~~~~~^~~~~~~~~~~~~~~~~ testvec.c:19:19: note: within this loop for (i = 0; i < 17; i ++) { ~~^~~~
في
-O9 ، يختفي هذا التحذير بطريقة أو بأخرى. ربما تكون الحقيقة هي أنه في مستويات عالية من التحسين ، يفرض المترجم بقوة أكبر نشر الحلقة. من الممكن (ولكن غير دقيق) أن يكون هذا خطأ في مجلس التعاون الخليجي (بمعنى فقدان التحذير ؛ وبالتالي ، فإن تصرفات مجلس التعاون الخليجي في أي حال لا تتعارض مع هذا المعيار ، لأنه لا يتطلب إصدار "تشخيصات" في مثل هذه الحالة).
الخلاصة: إذا كنت تكتب الشفرة في C أو C ++ ، فاحرص بشدة وتجنب المواقف التي تؤدي إلى سلوك غير محدد ، حتى عندما يبدو أنه "لا بأس به".
تعد أنواع الأعداد الصحيحة غير الموقعة مساعدًا جيدًا في العمليات الحسابية ، نظرًا لأنها مضمونة الدلالات المعيارية (لكن لا يزال بإمكانك مواجهة المشكلات المتعلقة بتمديد أنواع الأعداد الصحيحة). خيار آخر - لسبب ما لا يحظى بشعبية - هو عدم الكتابة في C و C ++ على الإطلاق. لعدة أسباب ، هذا الحل غير مناسب دائمًا. ولكن إذا كان يمكنك اختيار اللغة التي تريد كتابة البرنامج بها ، أي عندما تبدأ مشروعًا جديدًا على نظام أساسي يدعم Go أو Rust أو Java أو لغات أخرى ، فقد يكون من الأفضل أن ترفض استخدام C كـ "اللغة الافتراضية". اختيار الأدوات ، بما في ذلك لغة البرمجة ، هو دائما حل وسط. المطبات من C ، وخاصة السلوك غير المحدد في العمليات مع أنواع موقعة ، تؤدي إلى تكاليف إضافية لمواصلة صيانة الكود ، والتي غالبا ما يتم التقليل من قيمتها.