Satu ekspresi C++ bisa memuat banyak operator, dan compiler harus memutuskan mana yang dikerjakan lebih dulu. Dua aturan yang menentukannya adalah precedence (kekuatan ikatan: operator yang lebih kuat dikelompokkan lebih dulu) dan associativity (arah pengelompokan bila operatornya sederajat: kiri-ke-kanan atau kanan-ke-kiri). Ada satu pengetahuan yang sering salah dipahami: precedence menentukan pengelompokan, bukan urutan kapan operand dievaluasi — dua hal yang berbeda, dan kita akan membuktikan bedanya. Pada Chapter ini kita merangkum tabel precedence, menguji associativity di kedua arah, dan menutup Section Operator dengan aturan praktis menulis ekspresi yang tak bisa disalahpahami.
Tabel Precedence Ringkas #
Tabel berikut mengurutkan operator yang sudah kita kenal, dari yang paling kuat (dikelompokkan lebih dulu) ke yang paling lemah. Daftar lengkap semua operator ada pada Chapter Precedence dan Associativity di rujukan cppreference; versi ringkas ini mencakup yang sering dipakai.
| Kelompok | Operator | Associativity |
|---|---|---|
| postfix | i++ i-- pemanggilan fungsi () | kiri |
| unary | +x -x ! ~ ++i --i sizeof | kanan |
| multiplicative | * / % | kiri |
| additive | + - | kiri |
| relational | < > <= >= | kiri |
| equality | == != | kiri |
| logical AND | && | kiri |
| logical OR | || | kiri |
| conditional | ? : | kanan |
| assignment | = += -= *= /= %= &= |= ^= <<= >>= | kanan |
Dua pola yang layak dihafal: aritmetika lebih kuat dari perbandingan, dan perbandingan lebih kuat dari logika — sehingga a + 1 > b && c < d sudah terbaca benar tanpa satu kurung pun.
#include <iostream>
// Fungsi penanda untuk demonstrasi urutan evaluasi pada <<
int f() {
std::cout << "f() dipanggil; ";
return 1;
}
int g() {
std::cout << "g() dipanggil; ";
return 2;
}
int main() {
std::cout << std::boolalpha;
// Precedence: * lebih kuat dari +
std::cout << "2 + 3 * 4 = " << 2 + 3 * 4 << "\n";
std::cout << "(2 + 3) * 4 = " << (2 + 3) * 4 << "\n\n";
// Unary minus mengikat lebih kuat dari *
int a = 2;
std::cout << "-a * 3 = " << -a * 3 << "\n\n";
// Relational lebih kuat dari equality (kurung menunjukkan pengelompokan)
std::cout << "(3 > 2) == 1 = " << ((3 > 2) == 1) << "\n\n";
// Associativity kiri-ke-kanan pada - dan /
std::cout << "10 - 4 - 3 = " << 10 - 4 - 3 << "\n";
std::cout << "100 / 10 / 5 = " << 100 / 10 / 5 << "\n\n";
// Associativity kanan-ke-kiri pada assignment
int x = 0;
int y = 0;
int z = 0;
x = y = z = 9;
std::cout << "x = " << x << ", y = " << y << ", z = " << z << "\n\n";
// Urutan evaluasi << dijamin kiri ke kanan sejak C++17
std::cout << "[" << f() << "] [" << g() << "]\n";
return 0;
}Output yang Dihasilkan:
2 + 3 * 4 = 14
(2 + 3) * 4 = 20
-a * 3 = -6
(3 > 2) == 1 = true
10 - 4 - 3 = 3
100 / 10 / 5 = 2
x = 9, y = 9, z = 9
[f() dipanggil; 1] [g() dipanggil; 2]Mari kita bedah:
- Baris 20-21:
2 + 3 * 4menghasilkan14karena*lebih kuat (2 + (3 * 4)); kurung pada baris kedua membalik pengelompokan menjadi20. - Baris 24-25: unary minus mengikat lebih kuat daripada
*, sehingga-a * 3berarti(-2) * 3yaitu-6. - Baris 28: relational lebih kuat dari equality, jadi
3 > 2 == 1oleh compiler dibaca persis(3 > 2) == 1yang menghasilkantrue. Kurung pada kode kita hanya menuliskan pengelompokan bawaan itu secara eksplisit. - Baris 31-32: operator
-dan/bersifat kiri-ke-kanan, sehingga10 - 4 - 3berarti(10 - 4) - 3 = 3dan100 / 10 / 5berarti(100 / 10) / 5 = 2. Kalau asosiasinya dibaca kebalik, hasilnya9dan20. - Baris 36-40: assignment bersifat kanan-ke-kiri, sehingga
x = y = z = 9mengisizlebih dulu lalu meneruskan hasilnya keydanx— ketiganya9(sama seperti Chapter Operator Penugasan). - Baris 43: penanda
f()tercetak, lalu1disisipkan, kemudian penandag(), lalu2. Urutan dalam baris output itu adalah jaminan evaluasi<<dari kiri ke kanan sejak C++17.
Precedence Bukan Urutan Evaluasi #
Ini titik yang paling sering tertukar. Precedence hanya menentukan bagaimana ekspresi dikelompokkan — operand mana yang “dipasangkan” dengan operator mana. Ia tidak menentukan kapan sisi kiri dan sisi kanan dievaluasi. Untuk kebanyakan operator, seperti +, standar memang tidak mengatur urutan evaluasi operandnya:
// Compiler boleh mengevaluasi g() lebih dulu — boleh juga f().
// Pada satu compiler hasilnya tampak konsisten, tetapi standar
// tidak menjamin apa pun, jadi jangan bergantung padanya.
int hasil = f() + g();Pada operator &&, ||, dan ternary, urutan malah dijamin (kiri dulu, dengan short-circuit); dan sejak C++17, rantai <<, >>, penugasan, serta beberapa akses member juga dijamin kiri-ke-kanan — itulah yang membuat output [f() dipanggil; 1] [g() dipanggil; 2] pada program kita pasti. Tapi untuk f() + g(), f() * g(), atau pemanggilan fungsi cetak(f(), g()), urutan tetap tidak dijamin. Aturan amannya sederhana: jangan pernah menulis kode yang benarnya bergantung pada urutan evaluasi yang tidak dijamin.
Gunakan Kurung demi Kejelasan #
Compiler tidak butuh kurung untuk menghitung benar — tabel precedence-nya lengkap. Pembaca kode kita yang butuh. Ekspresi seperti x << 2 + 1 atau flag & mask == 0 (yang pengelompokannya hampir selalu berbeda dari niat penulisnya) adalah perangkap klasik; GCC bahkan menyediakan peringatan -Wparentheses untuk pola campuran yang rawan, seperti saat && dan || bercampur tanpa kurung atau perbandingan relasional bersarang di dalam == — peringatan yang kita temui sendiri saat menyusun contoh Chapter ini.
Dengan peta precedence ini, seluruh operator Section ini kini punya tempatnya. Satu topik tersisa untuk menutup Section: apa yang terjadi bila hasil perhitungan melewati batas tipe — topik overflow dan batasan angka.