Tidak semua kesalahan harus ditangani dengan return code yang sopan. Beberapa kesalahan menunjukkan bug programmer yang harus diperbaiki, bukan ditangani di runtime—misalnya pointer NULL yang seharusnya tidak mungkin terjadi bila pemanggil mengikuti kontrak, atau invariant internal yang dilanggar. Untuk kasus seperti ini, C menyediakan macro assert (dideklarasikan di <assert.h>) yang akan memanggil abort() bila kondisi yang diperiksa bernilai false. Pada chapter ini kita akan belajar cara memakai assert, lalu bagaimana menghilangkannya otomatis di production build lewat macro NDEBUG.
Kontrak assert #
Definisi assert di <assert.h> secara konseptual adalah:
#ifdef NDEBUG
#define assert(condition) ((void)0)
#else
#define assert(condition) \
(condition) ? (void)0 : __assert_fail(#condition, __FILE__, __LINE__, __func__)
#endifSaat NDEBUG tidak didefinisikan, assert(expr) mengevaluasi expr. Bila expr bernilai 0 (false), ia memanggil __assert_fail yang mencetak file, line number, nama function, ekspresi yang gagal, lalu memanggil abort(). Program berhenti dengan sinyal SIGABRT (exit code 134 pada shell).
Saat NDEBUG didefinisikan (biasanya lewat flag compiler -DNDEBUG), assert diekspansi menjadi ((void)0)—tidak ada evaluasi, tidak ada pengecekan, tidak ada pemanggilan function. Inilah cara standar mematikan semua assert sekaligus tanpa mengubah source code.
assert Dasar: Mendeteksi Pelanggaran Kontrak #
Mari kita lihat assert dalam aksi. Fungsi bagi() berikut punya kontrak: pemanggil harus memastikan b != 0. Bila pemanggil melanggar, assert akan memanggil abort().
#include <stdio.h>
#include <assert.h>
/* Fungsi bagi yang HANYA boleh dipanggil dengan b != 0.
assert berfungsi sebagai dokumentasi hidup yang dijalankan saat development. */
int bagi(int a, int b) {
assert(b != 0 && "pembagi tidak boleh nol");
return a / b;
}
int main(void) {
printf("100 / 5 = %d\n", bagi(100, 5));
printf("84 / 2 = %d\n", bagi(84, 2));
/* Saat ini dipanggil, assert akan gagal dan program abort */
printf("Memanggil bagi(10, 0)...\n");
int hasil = bagi(10, 0);
printf("Hasil = %d\n", hasil); /* tidak pernah tercapai */
return 0;
}Output yang Dihasilkan:
100 / 5 = 20
84 / 2 = 42
Memanggil bagi(10, 0)...
test_assert1: /tmp/test_assert1.c:7: bagi: Assertion `b != 0 && "pembagi tidak boleh nol"' failed.
Aborted (core dumped)Program mencetak dua baris pertama dengan benar, lalu saat bagi(10, 0) dipanggil, assert(b != 0 ...) mengevaluasi b != 0 menjadi false (karena b == 0). Macro assert kemudian memanggil __assert_fail yang mencetak pesan diagnostik lengkap dengan nama file, nomor baris, nama function, dan ekspresi yang gagal. Setelah itu abort() menghentikan proses dengan sinyal SIGABRT (shell melaporkan “Aborted”).
Mari kita telaah baris-baris kode di atas:
- Baris 7 (
assert(b != 0 && "pembagi tidak boleh nol");): ekspresib != 0 && "..."menggunakan trik klasik C. String literal"pembagi tidak boleh nol"selalu bernilai true (alamat string non-NULL), jadi kebenaran ekspresi ditentukan olehb != 0. Trik ini menambahkan pesan deskriptif ke outputasserttanpa mengubah logika. - Baris 13 (
printf("100 / 5 = %d\n", bagi(100, 5));): pemanggilan valid.b == 5sehinggaassertlulus diam-diam, hasil20dicetak. - Baris 17 (
int hasil = bagi(10, 0);): pemanggilan melanggar kontrak.b == 0,assertmemanggilabort(), dan baris 18 tidak pernah dijalankan. - Pesan diagnostik: format
nama_program: file:line: function: Assertion 'expr' failed.adalah kontrak yang ditentukan ISO/IEC 9899 §7.2.2.1. Pesan ini wajib ada di setiap implementation standar.
Matikan assert dengan NDEBUG #
Di production build, kita biasanya ingin menghapus seluruh pengecekan assert karena dua alasan: (1) overhead performa, dan (2) program yang crash dengan SIGABRT di produksi lebih buruk daripada melanjutkan dengan hasil yang mungkin salah. Caranya adalah mendefinisikan NDEBUG sebelum #include <assert.h> atau lewat flag compiler -DNDEBUG.
#include <stdio.h>
#include <assert.h>
int bagi(int a, int b) {
assert(b != 0 && "pembagi tidak boleh nol");
return a / b;
}
int main(void) {
/* Pakai stderr agar output tidak hilang saat abort() */
fprintf(stderr, "100 / 5 = %d\n", bagi(100, 5));
fprintf(stderr, "Memanggil bagi(10, 0)...\n");
int hasil = bagi(10, 0);
fprintf(stderr, "Hasil = %d (jika tercetak, assert dimatikan)\n", hasil);
return 0;
}Output yang Dihasilkan (build tanpa -DNDEBUG):
100 / 5 = 20
Memanggil bagi(10, 0)...
test_assert2: /tmp/test_assert2.c:5: bagi: Assertion `b != 0 && "pembagi tidak boleh nol"' failed.Output yang Dihasilkan (build dengan -DNDEBUG):
100 / 5 = 20
Memanggil bagi(10, 0)...
Floating point exception (core dumped)Perbedaannya dramatis:
- Tanpa
NDEBUG:assertmemicuabort(), exit code134(128 +SIGABRT=6). Pembagian oleh nol tidak sempat dieksekusi. - Dengan
NDEBUG:assertdihapus, kode melanjutkan kea / bdenganb == 0. Instruksiidivpada x86 memicu exceptionSIGFPE(sinyal 8), kernel menghentikan proses, exit code136(128 + 8).
Dua jenis kegagalan berbeda dengan dua pesan berbeda—jelas menunjukkan bahwa assert benar-benar dihapus saat NDEBUG didefinisikan.
assert untuk Invariant Internal #
Penggunaan paling berharga dari assert adalah memeriksa invariant internal—kondisi yang harus selalu benar pada titik tertentu program. Kita bisa menempatkan assert di awal fungsi (precondition), di akhir fungsi (postcondition), atau di tengah loop untuk memverifikasi asumsi struktur data.
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
/* Demonstrasi: assert untuk invariant internal (buffer tidak NULL,
kapasitas positif). Invariant adalah kondisi yang harus selalu
benar di titik tertentu program. */
typedef struct {
int *data;
size_t kapasitas;
size_t ukuran;
} buffer_t;
void buffer_push(buffer_t *b, int nilai) {
/* Precondition: buffer harus valid */
assert(b != NULL);
assert(b->data != NULL);
assert(b->ukuran <= b->kapasitas);
/* Invariant setelah penambahan: tidak overflow */
if (b->ukuran == b->kapasitas) {
/* Resize (sederhana - fixed demo) */
assert(0 && "buffer penuh, belum implementasi resize");
}
b->data[b->ukuran++] = nilai;
/* Postcondition */
assert(b->ukuran <= b->kapasitas);
}
int main(void) {
buffer_t buf;
buf.kapasitas = 2;
buf.ukuran = 0;
buf.data = malloc(sizeof(int) * buf.kapasitas);
assert(buf.data != NULL && "alokasi gagal");
buffer_push(&buf, 10);
buffer_push(&buf, 20);
fprintf(stderr, "buf[0] = %d, buf[1] = %d\n", buf.data[0], buf.data[1]);
fprintf(stderr, "ukuran = %zu, kapasitas = %zu\n", buf.ukuran, buf.kapasitas);
/* Push ketiga akan memicu assert(0) di buffer_push */
fprintf(stderr, "\nMencoba push ketiga...\n");
buffer_push(&buf, 30);
fprintf(stderr, "Seharusnya tidak tercetak\n");
free(buf.data);
return 0;
}Output yang Dihasilkan:
buf[0] = 10, buf[1] = 20
ukuran = 2, kapasitas = 2
Mencoba push ketiga...
test_assert3: /tmp/test_assert3.c:23: buffer_push: Assertion `0 && "buffer penuh, belum implementasi resize"' failed.buffer_push() memakai assert di tiga titik: precondition (b != NULL, b->data != NULL, b->ukuran <= b->kapasitas), unreachable code (assert(0 && "...") untuk kasus yang belum diimplementasi), dan postcondition (b->ukuran <= b->kapasitas setelah penambahan). Saat push ketiga dipanggil, b->ukuran == b->kapasitas sehingga blok if dieksekusi dan assert(0) memicu abort() dengan pesan “buffer penuh, belum implementasi resize”.