Unspecified Behavior adalah situasi di mana standar C menyediakan beberapa pilihan perilaku yang valid, tetapi tidak menentukan pilihan mana yang dipakai compiler. Berbeda dari undefined behavior (yang tidak ada jaminan apa pun), unspecified behavior memilih salah satu dari sekian opsi yang terdefinisi—setiap opsi menghasilkan program yang valid, tetapi pilihan bisa berbeda antar compiler atau bahkan antar level optimasi. Contoh paling umum: urutan evaluasi argumen fungsi. Pada chapter ini kita akan mendemonstrasikan unspecified behavior dengan urutan evaluasi argumen dan belajar cara menulis kode yang tidak bergantung padanya.

Urutan Evaluasi Argumen Fungsi #

Standar C tidak menentukan urutan evaluasi argumen fungsi. Pada f(a(), b(), c()), kompilator bebas memanggil a(), b(), c() dalam urutan apa pun—a, b, c, c, b, a, atau acak. Bila setiap argumen hanya membaca nilai tanpa side effect, urutan tidak penting. Tetapi bila argumen memiliki side effect (misalnya memodifikasi variabel global), urutan menentukan hasil akhir.

c
#include <stdio.h>

int side_effect_a(void) {
    printf("[a]");
    return 1;
}

int side_effect_b(void) {
    printf("[b]");
    return 2;
}

int side_effect_c(void) {
    printf("[c]");
    return 3;
}

/* Urutan evaluasi argumen: unspecified */
int sum3(int a, int b, int c) {
    return a + b + c;
}

int main(void) {
    /* Output urutan a, b, c tidak dijamin */
    printf("panggilan 1: ");
    int s1 = sum3(side_effect_a(), side_effect_b(), side_effect_c());
    printf(" = %d\n", s1);

    printf("panggilan 2: ");
    int s2 = sum3(side_effect_c(), side_effect_a(), side_effect_b());
    printf(" = %d\n", s2);

    return 0;
}

Output yang Dihasilkan:

text
panggilan 1: [c][b][a] = 6
panggilan 2: [b][a][c] = 6

Mari kita telaah baris-baris kode di atas:

  1. Baris 4-15 (side_effect_a, side_effect_b, side_effect_c): tiga fungsi yang mencetak tag dan mengembalikan nilai. Output [a], [b], [c] memperlihatkan urutan eksekusi.
  2. Baris 18-20 (int sum3(int a, int b, int c)): fungsi yang menjumlahkan tiga argumen. Tidak ada side effect—hanya mengembalikan hasil. Total selalu 6 terlepas dari urutan pemanggilan.
  3. Baris 25 (sum3(side_effect_a(), side_effect_b(), side_effect_c())): pemanggilan dengan urutan argumen a, b, c. Pada output, urutan eksekusi adalah [c][b][a]—kompilator GCC di x86-64 mengevaluasi argumen dari kanan ke kiri (cdecl calling convention).
  4. Baris 29 (sum3(side_effect_c(), side_effect_a(), side_effect_b())): pemanggilan dengan urutan argumen c, a, b. Urutan eksekusi [b][a][c]—kompilator tetap memproses dari kanan ke kiri. Total tetap 6 karena penjumlahan bersifat komutatif.

Unspecified Behavior yang Berdampak: Variabel Global #

Bila side effect argumen mempengaruhi variabel global yang juga dibaca di argumen lain, urutan evaluasi menentukan hasil akhir. Berikut demonstrasi yang menghasilkan output berbeda tergantung urutan.

c
#include <stdio.h>

int g = 0;

int modify_g(int x) {
    g += x;
    printf("modify_g(%d) dipanggil, g sekarang %d\n", x, g);
    return g;
}

int read_g(void) {
    printf("read_g dipanggil, g sekarang %d\n", g);
    return g;
}

int main(void) {
    /* Urutan evaluasi operand function calls: unspecified */
    g = 10;
    printf("sebelum: g=%d\n", g);
    int total = modify_g(5) + read_g();
    printf("modify_g(5) + read_g() = %d (g akhir=%d)\n", total, g);
    printf("\n");

    /* Coba urutan berbeda */
    g = 10;
    int total2 = read_g() + modify_g(5);
    printf("read_g() + modify_g(5) = %d (g akhir=%d)\n", total2, g);

    return 0;
}

Output yang Dihasilkan:

text
sebelum: g=10
modify_g(5) dipanggil, g sekarang 15
read_g dipanggil, g sekarang 15
modify_g(5) + read_g() = 30 (g akhir=15)

read_g dipanggil, g sekarang 10
modify_g(5) dipanggil, g sekarang 15
read_g() + modify_g(5) = 25 (g akhir=15)

Pada panggilan pertama modify_g(5) + read_g(), GCC mengevaluasi operand kiri dulu (modify_g(5) dipanggil, g menjadi 15), lalu operand kanan (read_g mengembalikan 15). Total 15 + 15 = 30. Pada panggilan kedua read_g() + modify_g(5), urutan tetap kiri-dulu: read_g mengembalikan 10 (g masih 10), lalu modify_g(5) membuat g menjadi 15. Total 10 + 15 = 25.

Kedua hasil—30 dan 25—adalah perilaku yang valid menurut standar C. Compiler lain mungkin menghasilkan 25 untuk panggilan pertama dan 30 untuk panggilan kedua. Kode yang mengasumsikan salah satu hasil adalah bug portabilitas.

Daftar Unspecified Behavior Lain #

Selain urutan evaluasi argumen, beberapa unspecified behavior lain yang sering ditemui:

  • Urutan evaluasi operand operator biner: a + b bisa mengevaluasi a atau b lebih dulu.
  • Urutan evaluasi operand && dan ||: kecuali short-circuit evaluation yang dijamin (operand kiri dievaluasi dulu; operand kanan hanya bila perlu).
  • Urutan side effect pada comma operator: dipisahkan oleh operator koma (,), evaluasi dilakukan kiri ke kanan, tetapi urutan side effect antar statement tetap unspecified.
  • Allocation alignment pada malloc: alignment blok yang dikembalikan bervariasi (minimal 16 byte di glibc x86-64, tetapi tidak dijamin sama di platform lain).
  • Representasi integer negatif: two’s complement, one’s complement, atau sign-magnitude (semua platform modern pakai two’s complement, tetapi standar mengizinkan lainnya).

Membedakan UB, IDB, dan Unspecified #

  • Undefined Behavior (UB): standar tidak memberi batasan apa pun. Compiler bebas melakukan apa saja, termasuk menghapus kode atau memformat hard disk. Contoh: signed overflow, dereferensi NULL pointer.
  • Implementation-Defined Behavior (IDB): compiler memilih salah satu opsi yang valid dan wajib mendokumentasikannya. Contoh: sizeof(int), endianness.
  • Unspecified Behavior: compiler memilih salah satu opsi yang valid tetapi tidak perlu mendokumentasikan pilihannya. Pilihan bisa berbeda antar versi compiler atau level optimasi. Contoh: urutan evaluasi argumen.