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.

KelompokOperatorAssociativity
postfixi++ i-- pemanggilan fungsi ()kiri
unary+x -x ! ~ ++i --i sizeofkanan
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.

cpp
#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:

text
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:

  1. Baris 20-21: 2 + 3 * 4 menghasilkan 14 karena * lebih kuat (2 + (3 * 4)); kurung pada baris kedua membalik pengelompokan menjadi 20.
  2. Baris 24-25: unary minus mengikat lebih kuat daripada *, sehingga -a * 3 berarti (-2) * 3 yaitu -6.
  3. Baris 28: relational lebih kuat dari equality, jadi 3 > 2 == 1 oleh compiler dibaca persis (3 > 2) == 1 yang menghasilkan true. Kurung pada kode kita hanya menuliskan pengelompokan bawaan itu secara eksplisit.
  4. Baris 31-32: operator - dan / bersifat kiri-ke-kanan, sehingga 10 - 4 - 3 berarti (10 - 4) - 3 = 3 dan 100 / 10 / 5 berarti (100 / 10) / 5 = 2. Kalau asosiasinya dibaca kebalik, hasilnya 9 dan 20.
  5. Baris 36-40: assignment bersifat kanan-ke-kiri, sehingga x = y = z = 9 mengisi z lebih dulu lalu meneruskan hasilnya ke y dan x — ketiganya 9 (sama seperti Chapter Operator Penugasan).
  6. Baris 43: penanda f() tercetak, lalu 1 disisipkan, kemudian penanda g(), lalu 2. 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:

cpp Ilustrasi: urutan panggil pada + tidak dijamin standar
// 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.