عمق حفرة الأرنب أو مقابلة C ++ في PVS-Studio

مقابلة على C ++ في PVS-Studio

المؤلف: أندريه كاربوف ، خاندليانتسفيليب هانديليانتس.

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

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

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

سنخبر اليوم قصة قصيرة حول كيف اتضح أن أحد الأسئلة الأولى التي طُرحت في المقابلة أعمق مما خططنا. لذلك ، نعرض هذا الرمز:

void F1()
{
  int i = 1;
  printf("%d, %d\n", i++, i++);
}

واسأل: "ماذا ستتم طباعته؟"

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

هذا الرمز في البداية نسبة مئوية ، ثم د ، ثم نسبة أخرى ، د ، ثم عصا ، ن ، ثم وحدتين.

من الواضح ، في مثل هذه الحالات ، تنتهي المقابلة بسرعة.

لذا عد الآن إلى المقابلة العادية :). غالبًا ما يجيبون على النحو التالي :

1 و 2 سيطبعون ،

هذه هي إجابة المستوى الداخلي. نعم ، يمكن بالطبع طباعة هذه القيم ، لكننا ننتظر الإجابة التالية تقريبًا :

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

في الواقع ، إذا كنت تستخدم Clang ، يمكنك الحصول على "1 ، 2".

وإذا كنت تستخدم GCC ، يمكنك الحصول على "2 ، 1".

ذات مرة جربنا مترجم MSVC ، وأنتج أيضًا "2 ، 1". لا توجد علامات على وجود مشكلة.

في الآونة الأخيرة ، ولأغراض عامة لجهة خارجية ، كان من الضروري مرة أخرى تجميع هذا الرمز باستخدام Visual C ++ الحديث وتشغيله. تم التجميع ضمن تكوين الإصدار مع تمكين تحسين O2 . وكما يقولون ، وجدوا مغامرة على رؤوسهم :). ما الذي تظن أنه حدث؟ ها! إليك ما يلي: "1 ، 1".

لذا فكر فيما تريد. اتضح أن السؤال أعمق وأكثر إرباكًا. نحن أنفسنا لم نتوقع حدوث ذلك.

نظرًا لأن معيار C ++ لا ينظم حساب الحجج بأي شكل من الأشكال ، فإن المترجم يفسر هذا النوع من السلوك غير المحدد بطريقة غريبة جدًا. دعونا نلقي نظرة على رمز المجمع الذي تم إنشاؤه بواسطة مترجم MSVC 19.25 (Microsoft Visual Studio Community 2019 ، الإصدار 16.5.1) ، إصدار العلم لمعيار اللغة هو '/ std: c ++ 14':


بشكل رسمي ، قام المُحسِّن بتحويل الرمز أعلاه إلى ما يلي:

void F1()
{
  int i = 1;
  int tmp = i;
  i += 2;
  printf("%d, %d\n", tmp, tmp);
}

من وجهة نظر المترجم ، لا يغير هذا التحسين السلوك المرصود للبرنامج. بالنظر إلى هذا ، تبدأ في فهم أنه ، لسبب وجيه ، أضاف معيار C ++ 11 ، بالإضافة إلى المؤشرات الذكية ، وظيفة "السحر" make_shared (كما أضاف C ++ 14 make_unique ). مثل هذا المثال غير ضار ، ويمكنه أيضًا "كسر الحطب":

void foo(std::unique_ptr<int>, std::unique_ptr<double>);

int main()
{
  foo(std::unique_ptr<int> { new int { 0 } },
      std::unique_ptr<double> { new double { 0.0 } });
}

يمكن للمترجم الماكر تحويل هذا إلى الترتيب التالي للحسابات (نفس MSVC ، على سبيل المثال):

new int { .... };
new double { .... };
std::unique_ptr<int>::unique_ptr
std::unique_ptr<double>::unique_ptr

إذا كانت المكالمة الثانية للعامل الجديد تطرح استثناء ، فإننا نحصل على تسرب للذاكرة.

ولكن نعود إلى الموضوع الأصلي. على الرغم من حقيقة أن كل شيء كان على ما يرام من وجهة نظر المترجم ، إلا أننا ما زلنا على يقين من أن الناتج "1 ، 1" سيكون غير صحيح للنظر في السلوك الذي يتوقعه المطور. ثم حاولنا تجميع شفرة المصدر باستخدام برنامج التحويل البرمجي MSVC مع علامة الإصدار القياسي '/ std: c ++ 17'. ويبدأ كل شيء في العمل كما هو متوقع ، وتتم طباعة "2 ، 1". ألق نظرة على كود المجمع:


كل شيء عادل ، قام المترجم بتمرير القيمتين 2 و 1. كحجج ، ولكن لماذا تغير كل شيء بشكل كبير؟ اتضح أنه تم إضافة ما يلي إلى معيار C ++ 17:

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

لا يزال المترجم لديه الحق في حساب الحجج بترتيب تعسفي ، ولكن الآن ، بدءًا من معيار C ++ 17 ، لديه الحق في البدء في حساب الوسيطة التالية وآثارها الجانبية فقط من لحظة اكتمال جميع الحسابات والآثار الجانبية للحجة السابقة.

بالمناسبة ، إذا قمت بتجميع المثال نفسه باستخدام مؤشرات ذكية مع علامة '/ std: c ++ 17' ، فإن كل شيء يصبح جيدًا هناك أيضًا - أصبح استخدام std :: make_unique اختياريًا الآن.

هنا مقياس آخر لعمق السؤال الذي اتضح. هناك نظرية ، ولكن هناك ممارسة في شكل مترجم محدد أو تفسير مختلف للمعيار :). إن عالم C ++ دائمًا أكثر تعقيدًا وغير متوقع مما يبدو.

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

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

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


إذا كنت ترغب في مشاركة هذه المقالة مع جمهور يتحدث الإنجليزية ، فيرجى استخدام رابط الترجمة: Andrey Karpov ، Phillip Khandeliants. كيف يذهب عمق حفرة الأرنب ، أو مقابلات العمل C ++ في PVS-Studio .

All Articles