Apa jawaban yang benar untuk cout << a ++ << a ;?

98

Baru-baru ini dalam sebuah wawancara ada pertanyaan tipe objektif berikut.

int a = 0;
cout << a++ << a;

Jawaban:

Sebuah. 10
b. 01
c. perilaku tidak terdefinisi

Saya menjawab pilihan b, yaitu output akan menjadi "01".

Tapi yang mengejutkan saya kemudian saya diberitahu oleh pewawancara bahwa jawaban yang benar adalah opsi c: tidak ditentukan.

Sekarang, saya tahu konsep poin urutan di C ++. Perilaku tidak ditentukan untuk pernyataan berikut:

int i = 0;
i += i++ + i++;

tetapi sesuai pemahaman saya untuk pernyataan itu cout << a++ << a, ostream.operator<<()itu akan dipanggil dua kali, pertama dengan ostream.operator<<(a++)dan kemudian ostream.operator<<(a).

Saya juga memeriksa hasilnya pada kompiler VS2010 dan hasilnya juga '01'.

pravs
sumber
30
Apakah Anda meminta penjelasan? Saya sering mewawancarai calon potensial dan saya cukup tertarik untuk menerima pertanyaan, itu menunjukkan ketertarikan.
Brady
3
@jrok Ini perilaku yang tidak terdefinisi. Apa pun yang dilakukan implementasi (termasuk mengirim email yang menghina nama Anda ke atasan Anda) adalah sesuai.
James Kanze
2
Pertanyaan ini menangis keluar untuk C ++ 11 (yang saat ini versi C ++) jawaban yang tidak menyebutkan urutan poin. Sayangnya saya kurang memiliki pengetahuan tentang penggantian sequence point di C ++ 11.
CB Bailey
3
Jika tidak tidak ditentukan, pasti tidak bisa 10, itu akan menjadi salah satu 01atau 00. ( c++Akan selalu mengevaluasi nilai cmemiliki sebelum yang bertambah). Dan bahkan jika itu tidak terdefinisi, itu masih akan sangat membingungkan.
kiri sekitar
2
Ya tahu, ketika saya membaca judul “cout << c ++ << c”, saya sejenak menganggapnya sebagai pernyataan tentang hubungan antara bahasa C dan C ++, dan yang lainnya bernama “cout”. Anda tahu, seperti seseorang mengatakan bagaimana mereka berpikir bahwa "cout" jauh lebih rendah daripada C ++, dan bahwa C ++ jauh lebih rendah daripada C - dan mungkin dengan transitivitas bahwa "cout" sangat, sangat lebih rendah daripada C. :)
tchrist

Jawaban:

145

Anda bisa memikirkan:

cout << a++ << a;

Sebagai:

std::operator<<(std::operator<<(std::cout, a++), a);

C ++ menjamin bahwa semua efek samping dari evaluasi sebelumnya akan dilakukan pada titik-titik urutan . Tidak ada titik urutan di antara evaluasi argumen fungsi yang berarti bahwa argumen adapat dievaluasi sebelum std::operator<<(std::cout, a++)atau sesudah argumen . Jadi hasil di atas tidak terdefinisi.


Pembaruan C ++ 17

Di C ++ 17 aturan telah diperbarui. Khususnya:

Dalam ekspresi operator shift E1<<E2dan E1>>E2, setiap perhitungan nilai dan efek samping E1diurutkan sebelum setiap perhitungan nilai dan efek samping E2.

Yang berarti membutuhkan kode untuk menghasilkan hasil b, yang mana keluaran 01.

Lihat Urutan Evaluasi Ekspresi Pemurnian P0145R3 untuk C ++ Idiomatik untuk lebih jelasnya.

Maxim Egorushkin
sumber
@Maxim: Terima kasih atas penjelasannya. Dengan panggilan yang Anda jelaskan, itu akan menjadi perilaku yang tidak ditentukan. Tapi sekarang, saya punya satu pertanyaan lagi (mungkin siller satu, dan saya kehilangan sesuatu yang mendasar dan berpikir keras) Bagaimana Anda menyimpulkan bahwa versi global std :: operator << () akan dipanggil alih-alih ostream :: operator < <() versi anggota. Saat debugging saya mendarat di versi anggota panggilan ostream :: operator << () daripada versi global dan itulah alasan yang awalnya saya pikir jawabannya adalah 01
pravs
@Maxim Bukan berarti membuat yang berbeda, tapi karena cmemiliki tipe int, operator<<inilah fungsi anggota.
James Kanze
2
@pravs: apakah operator<<fungsi anggota atau fungsi berdiri bebas tidak mempengaruhi titik urutan.
Maxim Egorushkin
11
'Titik urutan' tidak lagi digunakan dalam standar C ++. Itu tidak tepat dan telah diganti dengan hubungan 'diurutkan sebelum / diurutkan setelah'.
Rafał Dowgird
2
So the result of the above is undefined.Penjelasan Anda hanya bagus untuk yang tidak ditentukan , bukan untuk yang tidak ditentukan . JamesKanze menjelaskan bagaimana semakin memberatkan terdefinisi dalam jawabannya meskipun .
Deduplicator
68

Secara teknis, secara keseluruhan ini adalah Perilaku yang Tidak Terdefinisi .

Namun, ada dua aspek penting jawabannya.

Pernyataan kode:

std::cout << a++ << a;

dievaluasi sebagai:

std::operator<<(std::operator<<(std::cout, a++), a);

Standar tidak menentukan urutan evaluasi argumen ke suatu fungsi.
Jadi salah satu:

  • std::operator<<(std::cout, a++) dievaluasi terlebih dahulu atau
  • adievaluasi terlebih dahulu atau
  • itu mungkin saja urutan implementasi yang ditentukan.

Pesanan ini Tidak Ditentukan [Ref 1] sesuai standar.

[Ref 1] C ++ 03 5.2.2 Panggilan fungsi
Para 8

Urutan evaluasi argumen tidak ditentukan . Semua efek samping dari evaluasi ekspresi argumen berlaku sebelum fungsi dimasukkan. Urutan evaluasi ekspresi postfix dan daftar ekspresi argumen tidak ditentukan.

Selanjutnya, tidak ada titik urutan antara evaluasi argumen ke fungsi tetapi titik urutan hanya ada setelah evaluasi semua argumen [Ref 2] .

[Ref 2] C ++ 03 1.9 Eksekusi program [intro.execution]:
Para 17:

Saat memanggil fungsi (baik fungsi sebaris atau tidak), ada titik urutan setelah evaluasi semua argumen fungsi (jika ada) yang terjadi sebelum eksekusi ekspresi atau pernyataan apa pun dalam badan fungsi.

Perhatikan bahwa, di sini nilai csedang diakses lebih dari sekali tanpa titik urutan yang mengganggu, standarnya mengatakan:

[Ref 3] C ++ 03 5 Ekspresi [expr]:
Para 4:

....
Antara titik urutan sebelumnya dan berikutnya, nilai simpanan objek skalar harus dimodifikasi paling banyak satu kali dengan evaluasi ekspresi. Lebih lanjut, nilai sebelumnya harus diakses hanya untuk menentukan nilai yang akan disimpan . Persyaratan paragraf ini harus dipenuhi untuk setiap urutan subekspresi lengkap yang diizinkan; jika tidak, perilakunya tidak terdefinisi .

Kode memodifikasi clebih dari sekali tanpa mengintervensi titik urutan dan tidak sedang diakses untuk menentukan nilai objek yang disimpan. Ini jelas merupakan pelanggaran terhadap klausul di atas dan karenanya hasil seperti yang diamanatkan oleh standar adalah Perilaku Tidak Terdefinisi [Ref 3] .

Alok Save
sumber
1
Secara teknis, perilaku tidak terdefinisi, karena ada modifikasi objek, dan mengaksesnya di tempat lain tanpa titik urutan yang mengganggu. Terdefinisi adalah tidak ditentukan; hal itu membuat penerapan lebih leluasa.
James Kanze
1
@Ya. Saya belum melihat hasil edit Anda (meskipun saya bereaksi terhadap pernyataan Jrok bahwa program tidak dapat melakukan sesuatu yang aneh --- ia dapat). Versi editan Anda sejauh ini bagus, tetapi menurut saya, kata kuncinya adalah urutan parsial ; poin urutan hanya memperkenalkan urutan parsial.
James Kanze
1
@Als thanks for an elaborative description, really very helpful !!
pravs
4
Standar C ++ 0x baru mengatakan pada dasarnya sama tetapi di bagian yang berbeda dan dalam kata-kata yang berbeda :) Kutipan: (1.9 Program Execution [intro.execution], par 15): "Jika efek samping pada objek skalar tidak diurutkan relatif terhadap baik efek samping lain pada objek skalar yang sama atau penghitungan nilai menggunakan nilai objek skalar yang sama, perilaku ini tidak ditentukan. "
Rafał Dowgird
2
Saya yakin ada bug dalam jawaban ini. "std :: cout << c ++ << c;" tidak bisa menerjemahkan ke "std :: operator << (std :: operator << (std :: cout, c ++), c)", karena std :: operator << (std :: ostream &, int) tidak ada. Sebaliknya, ini diterjemahkan menjadi "std :: cout.operator << (c ++). Operator (c);", yang sebenarnya memiliki titik urutan antara evaluasi "c ++" dan "c" (operator yang kelebihan beban dianggap sebagai panggilan fungsi dan karena itu ada titik urutan ketika panggilan fungsi kembali). Akibatnya perilaku dan eksekusi order yang ditentukan.
Christopher Smith
20

Poin urutan hanya menentukan urutan parsial . Dalam kasus Anda, Anda memiliki (setelah resolusi kelebihan beban selesai):

std::cout.operator<<( a++ ).operator<<( a );

Ada titik urutan antara a++panggilan pertama dan ke std::ostream::operator<<, dan ada titik urutan antara apanggilan kedua dan kedua ke std::ostream::operator<<, tetapi tidak ada titik urutan antara a++dan a; satu-satunya batasan urutan adalah yang a++sepenuhnya dievaluasi (termasuk efek samping) sebelum panggilan pertama ke operator<<, dan yang kedua adievaluasi sepenuhnya sebelum panggilan kedua ke operator<<. (Ada juga batasan urutan kausual: panggilan kedua ke operator<<tidak dapat mendahului yang pertama, karena ini membutuhkan hasil dari yang pertama sebagai argumen.) §5 / 4 (C ++ 03) menyatakan:

Kecuali jika disebutkan, urutan evaluasi operand dari masing-masing operator dan subekspresi dari ekspresi individu, dan urutan efek samping yang terjadi, tidak ditentukan. Antara titik urutan sebelumnya dan berikutnya, nilai simpanan objek skalar harus dimodifikasi paling banyak sekali dengan evaluasi ekspresi. Lebih lanjut, nilai sebelumnya harus diakses hanya untuk menentukan nilai yang akan disimpan. Persyaratan paragraf ini harus dipenuhi untuk setiap urutan subekspresi lengkap yang diizinkan; jika tidak, perilaku tidak terdefinisi.

Salah satu urutan ekspresi Anda yang diperbolehkan adalah a++,, apanggilan pertama ke operator<<, panggilan kedua ke operator<<; ini mengubah nilai yang disimpan dari a( a++), dan mengaksesnya selain untuk menentukan nilai baru (yang kedua a), perilakunya tidak ditentukan.

James Kanze
sumber
Satu tangkapan dari kutipan standar Anda. IIRC, "kecuali jika dicatat", menyertakan pengecualian saat menangani operator yang kelebihan beban, yang memperlakukan operator sebagai fungsi dan karena itu membuat titik urutan antara panggilan pertama dan kedua ke std :: ostream :: operator << (int ). Tolong koreksi saya jika saya salah.
Christopher Smith
@ChristopherSmith Operator yang kelebihan beban berperilaku seperti pemanggilan fungsi. Jika cadalah tipe pengguna dengan pengguna yang ditentukan ++, inthasilnya tidak akan ditentukan, tetapi tidak akan ada perilaku yang tidak ditentukan.
James Kanze
1
@ChristopherSmith Di mana Anda melihat titik urutan antara keduanya cdalam foo(foo(bar(c)), c)? Ada titik urutan ketika fungsi dipanggil, dan kapan mereka kembali, tetapi tidak ada pemanggilan fungsi yang diperlukan di antara evaluasi keduanya c.
James Kanze
1
@ChristopherSmith Jika cUDT, operator yang kelebihan beban akan menjadi pemanggilan fungsi, dan akan memperkenalkan titik urutan, sehingga perilaku tidak akan terdefinisi. Tapi masih belum ditentukan apakah sub-ekspresi cdievaluasi sebelum atau sesudah c++, jadi apakah Anda mendapatkan versi tambahan atau tidak tidak akan ditentukan (dan secara teori, tidak harus sama setiap saat).
James Kanze
1
@ChristopherSmith Segala sesuatu sebelum titik urutan akan terjadi sebelum apa pun setelah titik urutan. Tetapi poin urutan hanya menentukan urutan parsial. Dalam ekspresi yang dimaksud, misalnya, tidak ada titik urutan antara sub-ekspresi cdan c++, jadi keduanya dapat muncul dalam urutan apa pun. Adapun titik koma ... Mereka hanya menyebabkan titik urutan sejauh ekspresi penuh. Titik urutan penting lainnya adalah pemanggilan fungsi: f(c++)akan melihat bertambahnya cmasuk f, dan operator koma &&, ||dan ?:juga menyebabkan titik urutan.
James Kanze
4

Jawaban yang benar adalah mempertanyakan pertanyaan itu. Pernyataan tersebut tidak dapat diterima karena pembaca tidak dapat melihat jawaban yang jelas. Cara lain untuk melihatnya adalah bahwa kami telah memperkenalkan efek samping (c ++) yang membuat pernyataan tersebut jauh lebih sulit untuk ditafsirkan. Kode yang ringkas itu bagus, asalkan artinya jelas.

Paul Marrington
sumber
4
Pertanyaan tersebut mungkin menunjukkan praktik pemrograman yang buruk (dan bahkan C ++ yang tidak valid). Tetapi sebuah jawaban seharusnya menjawab pertanyaan yang menunjukkan apa yang salah dan mengapa itu salah. Sebuah komentar atas pertanyaan bukanlah sebuah jawaban meskipun itu benar-benar valid. Paling banter, ini bisa berupa komentar, bukan jawaban.
PP