Mengapa operator terner dengan koma hanya mengevaluasi satu ekspresi dalam kasus yang sebenarnya?

119

Saat ini saya belajar C ++ dengan buku C ++ Primer dan salah satu latihan di buku ini adalah:

Jelaskan apa yang dilakukan ekspresi berikut: someValue ? ++x, ++y : --x, --y

Apa yang kita ketahui? Kita tahu bahwa operator terner memiliki prioritas yang lebih tinggi daripada operator koma. Dengan operator biner, hal ini cukup mudah untuk dipahami, tetapi dengan operator terner saya sedikit kesulitan. Dengan operator biner "memiliki prioritas lebih tinggi" berarti kita dapat menggunakan tanda kurung di sekitar ekspresi dengan prioritas lebih tinggi dan itu tidak akan mengubah eksekusi.

Untuk operator terner, saya akan melakukan:

(someValue ? ++x, ++y : --x, --y)

secara efektif menghasilkan kode yang sama yang tidak membantu saya dalam memahami bagaimana kompilator akan mengelompokkan kode.

Namun, dari pengujian dengan compiler C ++ saya tahu bahwa ekspresi mengkompilasi dan saya tidak tahu apa yang :bisa dipegang oleh operator dengan sendirinya. Jadi kompilator tampaknya menafsirkan operator terner dengan benar.

Kemudian saya menjalankan program dengan dua cara:

#include <iostream>

int main()
{
    bool someValue = true;
    int x = 10, y = 10;

    someValue ? ++x, ++y : --x, --y;

    std::cout << x << " " << y << std::endl;
    return 0;
}

Hasil dalam:

11 10

Sedangkan di sisi lain dengan someValue = falseitu mencetak:

9 9

Mengapa compiler C ++ menghasilkan kode yang untuk cabang-benar dari operator terner hanya bertambah x, sedangkan untuk cabang-palsu dari terner itu menurunkan keduanya xdan y?

Saya bahkan melangkah lebih jauh dengan menempatkan tanda kurung di sekitar cabang-asli seperti ini:

someValue ? (++x, ++y) : --x, --y;

tapi tetap saja hasilnya 11 10.

Aufziehvogel
sumber
5
"Precedence" hanyalah fenomena yang muncul di C ++. Mungkin lebih mudah untuk hanya melihat tata bahasa secara langsung dan melihat cara kerja ekspresi.
Kerrek SB
26
Kami tidak peduli bahwa banyak tentang prinsip-prinsip. :-) Fakta bahwa Anda harus menanyakan ini di sini menunjukkan bahwa kode tersebut tidak akan pernah lolos tinjauan kode oleh sesama pemrogram Anda. Itu membuat pengetahuan tentang bagaimana ini sebenarnya bekerja kurang bermanfaat. Kecuali jika Anda ingin berpartisipasi dalam Kontes C yang Dikaburkan , tentunya.
Bo Persson
5
@BoPersson tanpa contoh seperti ini untuk dipelajari, pengulas masa depan tidak akan pernah belajar mengapa mereka harus menolak ini dari produksi.
Leushenko
8
@Leushenko - Bagaimanapun juga, lonceng peringatan harus berbunyi. Beberapa kenaikan dan penurunan dalam pernyataan yang sama (ding, ding, ding!). Operator terner ketika Anda dapat menggunakan if-else (ding, ding, ding!). Tunggu, apakah koma itu operator koma yang ditakuti? (ding, DING, DING!) Dengan semua operator tersebut, mungkinkah ada beberapa hal yang diutamakan? (ding, ding, ding!) Jadi kita tidak akan pernah bisa menggunakan itu. Lalu mengapa membuang waktu untuk mencari tahu apa fungsinya, jika ada?
Bo Persson
4
Minor nit: namanya ?adalah operator bersyarat . Istilah operator terner berarti operator dengan tiga operan. Operator bersyarat adalah salah satu contoh operator terner, tetapi suatu bahasa dapat (secara teoritis) memiliki beberapa operator terner.
bta

Jawaban:

122

Seperti yang dikatakan @Rakete dalam jawaban mereka yang luar biasa, ini rumit. Saya ingin menambahkan sedikit.

Operator terner harus berbentuk:

logis-atau-ekspresi ? ekspresi : tugas-ekspresi

Jadi kami memiliki pemetaan berikut:

  • someValue: logika-atau-ekspresi
  • ++x, ++y: ekspresi
  • ??? itu tugas-ekspresi --x, --y atau hanya --x?

Sebenarnya ini hanya --xkarena ekspresi tugas tidak dapat diurai sebagai dua ekspresi yang dipisahkan oleh koma (menurut aturan tata bahasa C ++), jadi --x, --ytidak dapat diperlakukan sebagai ekspresi tugas .

Yang menghasilkan bagian ekspresi terner (bersyarat) terlihat seperti ini:

someValue?++x,++y:--x

Mungkin membantu demi keterbacaan untuk mempertimbangkan ++x,++yuntuk dihitung seolah-olah dalam tanda kurung (++x,++y); apa pun yang terkandung di antara ?dan :akan diurutkan setelah bersyarat. (Saya akan memberi tanda kurung untuk sisa postingan).

dan dievaluasi dalam urutan ini:

  1. someValue?
  2. (++x,++y)atau --x(tergantung pada boolhasil 1.)

Ekspresi ini kemudian diperlakukan sebagai sub-ekspresi kiri ke operator koma, dengan sub-ekspresi kanan --y, seperti:

(someValue?(++x,++y):--x), --y;

Yang berarti sisi kiri adalah ekspresi nilai yang dibuang , artinya itu pasti dievaluasi, tetapi kemudian kami mengevaluasi sisi kanan dan mengembalikannya.

Jadi apa yang terjadi ketika someValueadalah true?

  1. (someValue?(++x,++y):--x)mengeksekusi dan meningkatkan xdan ymenjadi 11dan11
  2. Ekspresi kiri dibuang (meskipun efek samping kenaikan tetap ada)
  3. Kami mengevaluasi sisi kanan operator koma:, --yyang kemudian menurunkan ykembali ke10

Untuk "memperbaiki" perilaku, Anda dapat mengelompokkan --x, --ydengan tanda kurung untuk mengubahnya menjadi ekspresi utama yang merupakan entri valid untuk ekspresi-tugas *:

someValue?++x,++y:(--x, --y);

* Ini adalah rantai panjang yang agak lucu yang menghubungkan ekspresi penugasan kembali ke ekspresi utama:

assignment-expression --- (dapat terdiri dari) -> kondisional-ekspresi -> logika-atau-ekspresi -> logika-dan-ekspresi -> inklusif-atau-ekspresi -> eksklusif-atau-ekspresi - -> dan-ekspresi -> persamaan-ekspresi -> ekspresi-relasional -> ekspresi-pergeseran -> ekspresi-aditif -> ekspresi-perkalian -> ekspresi-pm -> ekspresi-cor -> unary-expression -> postfix-expression -> primary-expression

AndyG
sumber
10
Terima kasih telah bersusah payah menguraikan aturan tata bahasa; melakukan hal itu menunjukkan bahwa ada lebih banyak tata bahasa C ++ daripada yang akan Anda temukan di kebanyakan buku teks.
sdenham
4
@sdenham: Ketika orang bertanya mengapa "bahasa berorientasi ekspresi" itu bagus (yaitu ketika { ... }dapat diperlakukan sebagai ekspresi), saya sekarang memiliki jawaban => untuk menghindari keharusan memperkenalkan operator koma yang berperilaku sedemikian rumit.
Matthieu M.
Bisakah Anda memberi saya tautan untuk membaca tentang assignment-expressionrantai itu?
MiP
@MiP: Saya mengangkatnya dari standar itu sendiri, Anda dapat menemukannya di bawah gram.expr
AndyG
88

Wow, itu rumit.

Kompilator melihat ekspresi Anda sebagai:

(someValue ? (++x, ++y) : --x), --y;

Operator terner membutuhkan a :, ia tidak dapat berdiri sendiri dalam konteks itu, tetapi setelah itu, tidak ada alasan mengapa koma harus menjadi bagian dari kasus palsu.

Sekarang mungkin lebih masuk akal mengapa Anda mendapatkan keluaran itu. Jika someValuebenar, maka ++x, ++ydan --ydieksekusi, yang tidak secara efektif berubah ytetapi menambahkan satu ke x.

Jika someValuesalah, maka --xdan --ydieksekusi, mengurangi keduanya satu per satu.

Rakete1111
sumber
42

Mengapa compiler C ++ menghasilkan kode yang untuk cabang-sebenarnya dari operator terner hanya bertambah x

Anda salah menafsirkan apa yang telah terjadi. Cabang sejati menambah xdan y. Namun, ysegera dikurangi setelah itu, tanpa syarat.

Berikut adalah bagaimana hal ini terjadi: karena operator bersyarat memiliki prioritas yang lebih tinggi daripada operator koma di C ++ , kompilator mengurai ekspresi sebagai berikut:

   (someValue ? ++x, ++y : --x), (--y);
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^  ^^^^^

Perhatikan "yatim piatu" --ysetelah koma. Inilah yang menyebabkan penurunan yyang awalnya bertambah.

Saya bahkan melangkah lebih jauh dengan menempatkan tanda kurung di sekitar cabang-asli seperti ini:

someValue ? (++x, ++y) : --x, --y;

Anda berada di jalur yang benar, tetapi Anda memberi tanda kurung pada cabang yang salah: Anda dapat memperbaikinya dengan memberi tanda kurung pada cabang lain, seperti ini:

someValue ? ++x, ++y : (--x, --y);

Demo (cetakan 11 11)

dasblinkenlight
sumber
5

Masalah Anda adalah ekspresi terner tidak benar-benar memiliki prioritas yang lebih tinggi daripada koma. Faktanya, C ++ tidak dapat dideskripsikan secara akurat hanya dengan didahulukan - dan ini persis interaksi antara operator terner dan koma di mana ia dipecah.

a ? b++, c++ : d++

diperlakukan sebagai:

a ? (b++, c++) : d++

(koma berperilaku seolah-olah memiliki prioritas yang lebih tinggi). Di samping itu,

a ? b++ : c++, d++

diperlakukan sebagai:

(a ? b++ : c++), d++

dan operator terner lebih diutamakan.

Martin Bonner mendukung Monica
sumber
Saya pikir ini masih dalam ranah prioritas karena hanya ada satu parsing yang valid untuk garis tengah, bukan? Masih merupakan contoh yang membantu
sudo rm -rf slash
2

Poin yang terlewatkan dalam jawaban (meskipun disentuh pada komentar) adalah bahwa operator bersyarat selalu digunakan (dimaksudkan oleh desain?) Dalam kode nyata sebagai jalan pintas untuk menetapkan salah satu dari dua nilai ke variabel.

Jadi, konteks yang lebih besar adalah:

whatIreallyWanted = someValue ? ++x, ++y : --x, --y;

Yang tidak masuk akal di wajahnya, jadi kejahatannya bermacam-macam:

  • Bahasa memungkinkan efek samping yang konyol dalam suatu tugas.
  • Kompilator tidak memperingatkan Anda bahwa Anda melakukan hal-hal aneh.
  • Buku tersebut tampaknya berfokus pada pertanyaan 'jebakan'. Orang hanya dapat berharap bahwa jawaban di belakangnya adalah "Apa yang dilakukan ungkapan ini bergantung pada kasus tepi yang aneh dalam contoh yang dibuat-buat untuk menghasilkan efek samping yang tidak diharapkan siapa pun. Jangan pernah melakukan ini."
Taryn
sumber
1
Menugaskan salah satu dari dua variabel adalah kasus biasa terner operator, tetapi ada yang kesempatan ketika hal ini berguna untuk memiliki bentuk ekspresi if(misalnya, ekspresi kenaikan dalam untuk loop). Konteks yang lebih besar mungkin for (x = 0, y=0; x+y < 100; someValue?(++x, ++y) :( --x, --y))dengan loop yang dapat memodifikasi xdan yindependen.
Martin Bonner mendukung Monica
@MartinBonner Saya tidak yakin, dan contoh ini tampaknya membuat poin bo-perrson cukup baik, seperti yang dia kutip dari Tony Hoare.
Taryn