ما هي نقاط التسلسل في لغة C++ وما علاقتها بالسلوك غير المحدد؟

مدرب بايثون أونلاين للمبتدئين

تعلم Python بسهولة دون إرهاق نظري. حل مهام عملية مع التحقق التلقائي واكتب الكود مباشرة في المتصفح.

ابدأ الدورة

السلاح السري للمصحح: ما يغفل عنه مبرمجو C++

\n\n

هل سبق لك أن كتبت شيئًا مثل i = i++ + ++i; وحصلت على نتائج مختلفة في مترجمات مختلفة؟ أو واجهت خطأً غريبًا يظهر فقط على الخادم وليس على حاسوبك المحمول؟ تعرف على: السلوك غير المحدد (undefined behavior) — أحد أخطر أعداء مطوري C++. وفي قلب هذه الفوضى تقف نقاط التسلسل (sequence points).

\n\n

في هذه المقالة، سنشرح ما هي نقاط التسلسل، وكيف ترتبط بترتيب العمليات الحسابية في C++، والأهم من ذلك — كيف لا تقع في هذه الأخطاء. المادة مستندة إلى سؤال شهير على Stack Overflow حصل على آلاف المشاهدات. هيا بنا!

\n\n

ما هي نقاط التسلسل (sequence points)؟

\n\n

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

\n\n

في معيار C++ 98/03، تم استخدام مصطلح sequence point. في C++11، تم استبداله بعلاقات أكثر صرامة "sequenced before / sequenced after"، لكن الجوهر بقي: بين نقطتي تسلسل، يحق للمترجم إعادة ترتيب العمليات بالكامل، إذا لم يغير ذلك النتيجة (وفقًا لقاعدة "as-if").

\n\n

ببساطة: إذا حاولت تغيير نفس المتغير مرتين بين نقطتي تسلسل — فستحصل على سلوك غير محدد (undefined behavior). يمكن للمترجم أن يفعل أي شيء: من نتيجة صحيحة إلى تهيئة قرصك الصلب (مزاح، لكنه قريب من الحقيقة).

\n\n

أمثلة كلاسيكية على نقاط التسلسل

\n
    \n
  • الفاصلة المنقوطة ; — نهاية التعبير
  • \n
  • الفاصلة , كعامل (ولكن ليس في وسائط الدوال!)
  • \n
  • نهاية كل تعبير في for (بعد التهيئة، الفحص، والخطوة)
  • \n
  • العوامل المنطقية && و ||
  • \n
  • العامل الثلاثي ?:
  • \n
  • استدعاء الدالة (قبل أن يبدأ تنفيذ جسم الدالة)
  • \n
\n\n

كيف تولد نقاط التسلسل سلوكًا غير محدد؟

\n\n

هذا مثال كلاسيكي يجب أن يعرفه كل مطور C++:

\n\n
int i = 0;\ni = i++ + ++i;  // UB! سلوك غير محدد
\n\n

لماذا هذا سلوك غير محدد؟ لأنه بين نقطتي تسلسل (في هذه الحالة — بين بداية ونهاية التعبير) يتم تغيير المتغير i مرتين:

\n
    \n
  1. الزيادة اللاحقة i++
  2. \n
  3. الزيادة السابقة ++i
  4. \n
  5. الإسناد i = ...
  6. \n
\n\n

المترجم لا يعرف بأي ترتيب ينفذ هذه العمليات. النتيجة؟ مترجم سيعطي 2، وآخر 3، وثالث 0. وفي وضع التحسين قد يتجاهل هذا الكود تمامًا.

\n\n

حالة أخرى خادعة: وسائط الدالة

\n\n
int foo(int a, int b) { return a + b; }\n\nint x = 5;\nint result = foo(++x, ++x);  // UB!
\n\n

هنا الفاصلة بين الوسائط هي ليست عامل الفاصلة، بل فاصل. لا توجد نقطة تسلسل بين حساب الوسيطين ++x! كلا الزيادتين تُنفذان "بالتوازي" (من وجهة نظر المعيار)، ونحصل مرة أخرى على سلوك غير محدد.

\n\n

أمثلة عملية: كيف يبدو السلوك غير المحدد في الكود الحقيقي

\n\n

مثال 1: خطأ غير واضح في الفهرسة

\n\n
int arr[5] = {0, 10, 20, 30, 40};\nint idx = 1;\narr[idx] = idx++;  // UB!
\n\n

للوهلة الأولى، كل شيء بسيط: نريد كتابة 1 في arr[1]، ثم زيادة idx. لكن المعيار لا يضمن ترتيب حساب الفهرس idx والجزء الأيمن idx++. النتيجة؟ قد يُكتب في arr[1]، وقد يُكتب في arr[2].

\n\n

مثال 2: معقد

المدونات

توصيات الكتب