Kedalaman lubang kelinci atau wawancara C ++ di PVS-Studio

Wawancara pada C ++ di PVS-Studio

Penulis: Andrey Karpov, khandeliansPhilip Handelyants.

Saya ingin berbagi situasi yang menarik ketika pertanyaan yang kami gunakan pada wawancara ternyata lebih rumit dari yang dimaksudkan penulis. Dengan bahasa C ++ dan kompiler, Anda harus selalu waspada. Jangan bosan.

Seperti di perusahaan pemrograman lain, kami memiliki set pertanyaan untuk mewawancarai lowongan pengembang di C ++, C # dan Java. Kami memiliki banyak pertanyaan dengan double atau triple bottom. Untuk pertanyaan tentang C # dan Java, kami tidak dapat mengatakan dengan pasti, karena mereka memiliki penulis lain. Tetapi banyak pertanyaan yang dikompilasi oleh Andrei Karpov untuk wawancara dalam bahasa C ++ segera disusun untuk menyelidiki kedalaman pengetahuan fitur bahasa.

Pertanyaan-pertanyaan ini dapat diberikan jawaban yang benar sederhana. Anda bisa masuk lebih dalam dan lebih dalam. Bergantung pada ini, selama wawancara, kami menentukan seberapa banyak seseorang terbiasa dengan nuansa bahasa. Ini penting bagi kami, karena kami sedang mengembangkan penganalisa kode dan harus memahami dengan baik seluk beluk bahasa dan “lelucon”.

Hari ini kami akan menceritakan sebuah kisah pendek tentang bagaimana salah satu pertanyaan pertama yang diajukan pada wawancara ternyata lebih dalam dari yang kami rencanakan. Jadi, kami menunjukkan kode ini:

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

dan bertanya: "Apa yang akan dicetak?"

Pertanyaan bagus. Segera bisa mengatakan banyak tentang pengetahuan. Kasus ketika seseorang tidak bisa menjawabnya sama sekali tidak akan dipertimbangkan. Ini dihilangkan dengan pengujian pendahuluan di situs HeadHunter (hh.ru). Meskipun, tidak, itu omong kosong. Dalam ingatan kita, ada beberapa kepribadian unik yang menjawab sesuatu dalam semangat:

Kode ini akan mencetak di awal persentase, lalu d, lalu persentase lain, d, lalu tongkat, n, dan kemudian dua unit.

Jelas, dalam kasus seperti itu, wawancara berakhir dengan cepat.

Jadi, sekarang kembali ke wawancara normal :). Seringkali mereka menjawab seperti ini:

1 dan 2. Akan dicetak.

Ini adalah jawaban dari tingkat magang. Ya, nilai-nilai tersebut tentu saja dapat dicetak, tetapi kami menunggu kira-kira jawaban berikut:

Ini bukan untuk mengatakan apa tepatnya kode ini akan dicetak. Ini adalah perilaku yang tidak ditentukan (atau tidak ditentukan). Urutan di mana argumen dihitung tidak didefinisikan. Semua argumen harus dievaluasi sebelum tubuh fungsi dipanggil dieksekusi, tetapi urutan di mana ini akan terjadi diserahkan kepada kebijaksanaan kompiler. Oleh karena itu, kode dapat mencetak "1, 2" dan sebaliknya "2, 1". Secara umum, menulis kode seperti itu sangat tidak diinginkan, jika akan dibangun oleh setidaknya dua kompiler, dimungkinkan untuk "menembak di kaki". Dan banyak kompiler akan memberikan peringatan di sini.

Memang, jika Anda menggunakan Dentang , Anda bisa mendapatkan "1, 2".

Dan jika Anda menggunakan GCC , Anda bisa mendapatkan "2, 1".

Sekali waktu kami mencoba kompiler MSVC, dan juga menghasilkan "2, 1". Tidak ada tanda-tanda masalah.

Baru-baru ini, untuk tujuan pihak ketiga umum, sekali lagi perlu untuk mengkompilasi kode ini menggunakan Visual C ++ modern dan menjalankannya. Dirakit di bawah Konfigurasi rilis dengan / O2 optimisasi diaktifkan . Dan, seperti kata mereka, mereka menemukan petualangan di kepala mereka sendiri :). Menurutmu, apa yang terjadi? Ha! Inilah yang: "1, 1".

Jadi pikirkan apa yang Anda inginkan. Ternyata pertanyaannya bahkan lebih dalam dan membingungkan. Kami sendiri tidak berharap ini terjadi.

Karena standar C ++ tidak mengatur perhitungan argumen dengan cara apa pun, kompiler menafsirkan jenis perilaku yang tidak ditentukan ini dengan cara yang sangat aneh. Mari kita lihat kode assembler yang dihasilkan oleh kompiler MSVC 19.25 (Microsoft Visual Studio Community 2019, Versi 16.5.1), versi bendera dari standar bahasa adalah '/ std: c ++ 14':


Secara resmi, pengoptimal mengubah kode di atas menjadi sebagai berikut:

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

Dari sudut pandang kompiler, optimasi tersebut tidak mengubah perilaku program yang diamati. Melihat ini, Anda mulai memahami bahwa, untuk alasan yang baik, standar C ++ 11, selain smart pointer, juga menambahkan fungsi "ajaib" make_share (dan C ++ 14 juga menambahkan make_unique ). Contoh yang tidak berbahaya, dan juga dapat "memecah kayu bakar":

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 } });
}

Kompiler yang licik dapat mengubahnya menjadi urutan perhitungan berikut ( MSVC yang sama , misalnya):

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

Jika panggilan kedua ke operator baru melempar pengecualian, maka kami mendapatkan kebocoran memori.

Tapi kembali ke topik aslinya. Terlepas dari kenyataan bahwa semuanya baik-baik saja dari sudut pandang kompiler, kami masih yakin bahwa output "1, 1" akan salah untuk mempertimbangkan perilaku yang diharapkan oleh pengembang. Dan kemudian kami mencoba untuk mengkompilasi kode sumber dengan kompiler MSVC dengan bendera versi standar '/ std: c ++ 17'. Dan semuanya mulai berfungsi seperti yang diharapkan, dan "2, 1" dicetak. Lihatlah kode assembler:


Semuanya adil, kompiler memberikan nilai 2 dan 1. Sebagai argumen, tetapi mengapa semuanya berubah begitu dramatis? Ternyata yang berikut ini ditambahkan ke standar C ++ 17:

Ekspresi postfix diurutkan sebelum setiap ekspresi dalam daftar ekspresi dan argumen default apa pun. Inisialisasi parameter, termasuk setiap perhitungan nilai yang terkait dan efek samping, secara tidak pasti diurutkan sehubungan dengan parameter lainnya.

Kompiler masih memiliki hak untuk menghitung argumen dalam urutan acak, tetapi sekarang, mulai dari standar C ++ 17, ia memiliki hak untuk mulai menghitung argumen berikutnya dan efek sampingnya hanya dari saat semua perhitungan dan efek samping dari argumen sebelumnya selesai.

By the way, jika Anda mengkompilasi contoh yang sama dengan smart pointer dengan bendera '/ std: c ++ 17', maka semuanya menjadi baik-baik saja di sana - menggunakan std :: make_unique sekarang opsional.

Berikut ini adalah pengukuran kedalaman lainnya dalam pertanyaan yang ternyata. Ada sebuah teori, tetapi ada praktik dalam bentuk kompiler tertentu atau interpretasi standar yang berbeda :). Dunia C ++ selalu lebih kompleks dan tidak terduga daripada yang terlihat.

Jika seseorang dapat lebih akurat menjelaskan apa yang terjadi, maka tolong beri tahu kami di komentar. Kita akhirnya harus memahami pertanyaan untuk setidaknya kita tahu jawabannya di wawancara! :)

Berikut ini adalah cerita yang informatif. Kami harap ini menarik, dan Anda membagikan pendapat Anda tentang topik ini. Dan kami menyarankan menggunakan standar bahasa paling modern sebanyak mungkin, sehingga tidak terlalu terkejut bahwa kompiler pengoptimal saat ini dapat. Lebih baik lagi, jangan menulis kode ini sama sekali :).

PS Kita dapat mengatakan bahwa kita “menyalakan pertanyaan”, dan sekarang itu harus dihapus dari kuesioner. Kami tidak melihat intinya dalam hal ini. Jika seseorang tidak terlalu malas untuk mempelajari publikasi kita sebelum wawancara, membaca materi ini dan kemudian menggunakannya, maka dia dilakukan dengan baik dan sepatutnya akan menerima tanda plus :).


Jika Anda ingin berbagi artikel ini dengan audiens yang berbahasa Inggris, silakan gunakan tautan ke terjemahan: Andrey Karpov, Phillip Khandeliants. Bagaimana Deep the Rabbit Hole Goes, atau C ++ Job Wawancara di PVS-Studio .

All Articles