assert.h dan errno.h

Pemrograman defensif dengan assert dan idiom pemeriksaan error berbasis errno.

Daftar isi

Program yang tangguh memeriksa asumsinya secara eksplisit, bukan mengandalkan harapan bahwa semua berjalan sesuai rencana. Topik ini membahas dua header yang menyediakan pemeriksaan tersebut, yaitu assert.h untuk menjaga asumsi internal selama pengembangan dan errno.h untuk membaca laporan error dari fungsi pustaka standar. Keduanya membentuk dasar pemrograman defensif (defensive programming), yakni kebiasaan menetapkan apa yang dianggap sah dan merespons apa yang tidak sah secara terkendali.

Menjaga asumsi internal dengan assert#

Makro assert menerima satu ekspresi dan menganggap ekspresi itu wajib bernilai benar pada titik pemanggilannya. Apabila nilainya salah, makro mencetak pesan berisi nama berkas, nomor baris, nama fungsi, dan teks ekspresinya, lalu memanggil abort sehingga program berhenti. Makro ini tepat dipakai untuk asumsi internal, yaitu kondisi yang pelanggarannya berarti ada bug pada kode. Contohnya pointer yang tidak boleh kosong atau jumlah elemen yang tidak boleh nol. Contoh berikut memeriksa prasyarat sebuah fungsi perhitungan rata-rata:

c rata-rata-aman.c
#include <assert.h>
#include <stdio.h>

double rata_rata(const int *nilai, int jumlah)
{
    assert(nilai != NULL);
    assert(jumlah > 0);

    int total = 0;
    for (int i = 0; i < jumlah; i = i + 1) {
        total = total + nilai[i];
    }
    return (double)total / jumlah;
}

int main(void)
{
    int nilai_ujian[] = {70, 85, 90, 65, 80};
    int jumlah = (int)(sizeof nilai_ujian / sizeof nilai_ujian[0]);

    printf("Rata-rata: %.1f\n", rata_rata(nilai_ujian, jumlah));
    return 0;
}
text Keluaran
Rata-rata: 78.0

Kedua asumsi pada fungsi rata_rata terpenuhi, sehingga program berjalan normal dan mencetak hasilnya. Pemeriksaan itu memisahkan kesalahan pemanggil dari kesalahan data. Jumlah elemen nol pada fungsi ini berarti kode pemanggilnya keliru, bukan masukan pengguna yang wajar ditolak. Pembagian peran semacam ini menjadi garis pemisah antara assert dan penanganan error biasa.

Posisi assertion juga menjadi dokumentasi yang tidak pernah usang, karena setiap pembaca fungsi langsung melihat syarat yang dianggap pasti oleh penulisnya. Pesan yang dicetak makro sudah memuat lokasi dan ekspresinya, sehingga pelanggaran asumsi dilaporkan tanpa perlu menulis kode pelaporan tersendiri. Untuk keadaan yang wajar terjadi saat program berjalan, pemeriksaan biasa dengan pesan error tetap menjadi pilihan yang tepat.

Saat assertion gagal#

Perilaku assertion yang gagal paling mudah dipahami lewat demonstrasi langsung. Program berikut sengaja memuat satu assertion yang pasti gagal, dan eksekusinya dilakukan lewat terminal agar pesan serta kode keluarnya terlihat:

c cek.c
// tidak-dijalankan
#include <assert.h>
#include <stdio.h>

int main(void)
{
    int nilai = -3;
    printf("Program dimulai.\n");
    assert(nilai > 0);
    printf("Baris ini tidak pernah tercapai.\n");
    return 0;
}
bash
# kompilasi program pemeriksaan
gcc cek.c -o cek
# jalankan lewat sh, assertion gagal menghentikan program
sh -c './cek'
# lihat kode keluar yang dilaporkan shell
echo "kode keluar: $?"
text Keluaran
cek: cek.c:9: main: Assertion `nilai > 0' failed.
Aborted
kode keluar: 134

Pesan pada baris pertama memuat empat informasi, yaitu nama program, berkas cek.c, baris 9, dan ekspresi nilai > 0 yang ternyata salah. Keempatnya membuat titik kegagalan dapat langsung dituju pada debugger. Program dihentikan lewat sinyal SIGABRT sehingga kode keluarnya 134, yaitu 128 ditambah nomor sinyalnya. Baris Program dimulai. tidak ikut tercetak karena teks itu masih menunggu di buffer stdout ketika program dihentikan mendadak. Isi buffer baru sampai ke keluaran ketika program berakhir dengan normal.

Pada eksekusi lewat terminal interaktif, teks di buffer biasanya sempat tercetak karena terminal memakai penyanggaan per baris. Perbedaan kecil ini menegaskan satu hal, yaitu program yang dihentikan abort tidak menjamin isi buffer stdout-nya muncul. Bagi pembaca yang menjalankan contoh di atas pada terminal sendiri, pesan Aborted mungkin dilengkapi keterangan tambahan dari shell, tetapi baris assertion-nya tetap sama.

Mematikan assert pada build rilis#

Semua assertion dapat dihapus dari kode hasil kompilasi dengan mendefinisikan makro NDEBUG sebelum menyertakan header-nya. Definisi itu biasanya diberikan lewat flag kompilasi untuk build rilis, sehingga versi pengembangan tetap diperiksa sementara versi produksi bebas dari biaya pemeriksaannya:

c Contoh: menonaktifkan assertion
// tidak-dijalankan
#define NDEBUG            /* letakkan sebelum include assert.h */
#include <assert.h>
assert(nilai > 0);        /* diperluas menjadi kosong, tanpa biaya apa pun */

Karena seluruh assertion bisa hilang pada build rilis, kode di dalam assert tidak boleh memuat pekerjaan yang wajib tetap dilakukan, misalnya penambahan penghitung atau pemanggilan fungsi dengan efek samping. Aturan praktisnya, assert hanya untuk pemeriksaan, sedangkan pekerjaan sebenarnya tetap ditulis di luar makro tersebut.

Flag -DNDEBUG adalah bentuk pemberian makro ini lewat baris perintah, misalnya gcc -DNDEBUG program.c, dan cara itulah yang dipakai skrip build pada umumnya. Penempatan di dalam kode tetap sah, tetapi makronya hanya berpengaruh sebelum assert.h disertakan pada berkas itu, sehingga berkas lain mudah terlewat. Dengan mengatur flag tersebut di satu tempat, versi pengembangan dan versi rilis dibangun dari sumber yang sama tanpa penyuntingan manual.

errno: penanda error dari pustaka standar#

Header errno.h mendefinisikan variabel errno, yaitu bilangan bulat yang diisi oleh fungsi pustaka standar ketika terjadi error, ditambah makro konstanta untuk setiap kodenya. Nilai EDOM berarti argumen di luar wilayah sah, ERANGE berarti hasil di luar jangkauan, dan ENOENT berarti berkas atau direktori tidak ada. Nilai aktualnya berbeda antarsistem, sehingga kode program membandingkan errno dengan nama makronya, bukan dengan angkanya.

Aturan mainnya perlu diikuti agar pembacaan errno tidak menyesatkan. Nilai errno tidak pernah dikosongkan oleh fungsi pustaka ketika panggilannya berhasil. Pembacaan errno hanya bermakna setelah nilai kembali fungsinya menandakan kegagalan. Apabila satu nilai kembali bisa berarti berhasil sekaligus gagal, misalnya pada strtol yang tetap mengembalikan bilangan sah saat konversinya gagal, errno dikosongkan lebih dahulu sebelum pemanggilan:

c Contoh: membaca errno dengan benar
// tidak-dijalankan
errno = 0;
long nilai = strtol(teks, &akhir, 10);
if (akhir == teks) {
    printf("teks bukan angka sah\n");
} else if (errno == ERANGE) {
    printf("angka di luar jangkauan yang ditampung\n");
}

Program berikut memicu dua error yang kodenya pasti, yaitu sqrt pada argumen negatif dan fopen pada berkas yang tidak ada, lalu menampilkan kodenya lewat strerror:

c errno-demo.c
#include <errno.h>
#include <math.h>
#include <stdio.h>
#include <string.h>

int main(void)
{
    errno = 0;
    double hasil = sqrt(-1.0);
    if (isnan(hasil)) {
        printf("sqrt(-1.0) menghasilkan NaN\n");
    }
    printf("errno = %d (%s)\n", errno, strerror(errno));
    fflush(stdout);   /* pastikan urutan keluaran sebelum perror tetap pasti */

    FILE *berkas = fopen("arsip-tidak-ada.txt", "r");
    if (berkas == NULL) {
        perror("Gagal membuka arsip");
    } else {
        fclose(berkas);
    }
    return 0;
}
text Keluaran
sqrt(-1.0) menghasilkan NaN
errno = 33 (Numerical argument out of domain)
Gagal membuka arsip: No such file or directory

Fungsi strerror mengubah kode errno menjadi teks deskriptifnya, dan angka 33 pada keluaran adalah nilai EDOM di lingkungan ini. Fungsi perror menampilkan teks argumennya, diikuti titik dua dan pesan error untuk errno saat itu, sehingga satu panggilan sudah cukup untuk memberi tahu apa yang gagal dan mengapa. Panggilan fflush(stdout) sebelum perror menjaga urutan cetakannya tetap pasti. Alasannya, perror menulis ke stderr yang tidak dibuffer, sedangkan stdout pada eksekusi lewat pipa dibuffer penuh dan baru terbuang saat program berakhir. Error kedua terjadi karena berkas arsip-tidak-ada.txt memang tidak dibuat oleh contoh ini, sehingga ENOENT muncul dengan pasti pada setiap eksekusi.

Kesalahan umum#

Tiga kesalahan berikut paling sering muncul pada pemakaian pertama kedua header ini.

  • Memakai assert untuk memvalidasi masukan. Assertion menghentikan program dan lenyap saat NDEBUG aktif, sedangkan masukan pengguna yang salah adalah keadaan normal. Tangani masukan dengan pemeriksaan biasa, dan sisakan assert untuk asumsi internal yang pelanggarannya berarti bug.
  • Membaca errno tanpa melihat nilai kembali fungsinya. Karena fungsi yang berhasil tidak mengosongkan errno, nilai lama dari panggilan sebelumnya bisa tertinggal dan terbaca seolah error baru terjadi. Periksa dulu nilai kembinya, lalu baru tafsirkan errno.
  • Menaruh pekerjaan penting di dalam assert. Ekspresi seperti assert(atur_penghitung()) tidak akan pernah dipanggil pada build rilis, sehingga penghitungnya diam-diam tidak bertambah. Pindahkan pemanggilan fungsinya ke luar makro.

Ringkasan#

  • assert menjaga asumsi internal, mencetak lokasi dan ekspresinya saat gagal, lalu menghentikan program, dan seluruhnya dapat dimatikan dengan NDEBUG pada build rilis.
  • Kegagalan assertion menghentikan program lewat SIGABRT dengan kode keluar 134, dan buffer stdout yang belum terbuang tidak dijamin tercetak.
  • errno diisi oleh fungsi pustaka saat error, hanya bermakna setelah nilai kembali fungsinya menandakan kegagalan, dan dibandingkan dengan nama makro seperti EDOM, ERANGE, serta ENOENT.
  • strerror mengubah kode errno menjadi teks, sedangkan perror menampilkan pesan lengkap berupa argumen, titik dua, dan deskripsi errornya.

Referensi#