Machinery exception pada chapter-chapter sebelumnya dirancang untuk kegagalan yang bisa ditangani saat program berjalan. Kegagalan lain menuntut alat berbeda: kegagalan yang tidak seharusnya terjadi sama sekali. Bila sebuah function menerima vector kosong padahal kontraknya menolak kondisi tersebut, yang terjadi adalah bug programmer, bukan keadaan yang bisa diperbaiki pengguna. Untuk kategori ini C++ menyediakan macro assert yang bekerja saat program berjalan dan deklarasi static_assert yang bekerja saat kompilasi. Chapter ini membahas cara kerja keduanya, efek NDEBUG pada build debug dan release, serta kapan setiap alat dipakai.
assert: Menjaga Invariant Internal Saat Runtime #
Macro assert didefinisikan di header <cassert> dan menerima satu ekspresi. Bila ekspresi tidak sama dengan nol, program berlanjut tanpa efek apa pun; bila bernilai nol, macro mencetak pesan diagnostik ke standard error lalu memanggil std::abort() yang menghentikan program. Sasarannya adalah invariant: kondisi internal yang seharusnya selalu benar selama kode kita benar, misalnya prekondisi antar function internal. Program berikut memakai assert dengan benar — semua kondisi terpenuhi, sehingga program berjalan sampai selesai:
#include <cassert>
#include <iostream>
#include <vector>
// Function dengan prekondisi internal: antrean tidak boleh kosong.
// Pemanggilnya adalah kode kita sendiri, bukan input pengguna.
int nomorPertama(const std::vector<int>& antrean) {
assert(!antrean.empty() && "pemanggil wajib memeriksa antrean dulu");
return antrean[0];
}
int main() {
std::vector<int> antrean{101, 102, 103};
int pertama = nomorPertama(antrean);
std::cout << "nomor pertama: " << pertama << "\n";
// assert juga bisa menjaga hasil perhitungan internal.
int total = antrean[0] + antrean[1] + antrean[2];
assert(total == 306 && "total seharusnya 101 + 102 + 103");
std::cout << "total: " << total << "\n";
std::cout << "semua assert lolos, program berlanjut normal\n";
return 0;
}Output yang Dihasilkan:
nomor pertama: 101
total: 306
semua assert lolos, program berlanjut normalMari kita telaah poin-poin pentingnya:
- Baris 1 dan 8 (
#include <cassert>,assert(!antrean.empty() && "...")): header wajib agar macroasserttersedia, lalu pemeriksaan prekondisi di awal function; bila kondisi benar, program berlanjut tanpa efek tambahan. - Baris 9 (
return antrean[0];): akses elemen pertama sah karena prekondisi baru saja diperiksa — assert mendokumentasikan kontrak itu bagi pemanggil internal. - Baris 19-20 (perhitungan dan
assert(total == 306)): assert juga bisa menjaga hasil perhitungan — pengujian kecil yang menumpang di kode produksi. - Baris 23: pesan akhir membuktikan assert yang lolos tidak mengubah alur program.
Pola assert(kondisi && "pesan") pada baris 8 layak dicermati. String literal selalu bernilai benar, sehingga operator && tidak mengubah hasil pemeriksaan; perannya muncul hanya saat kegagalan, memperjelas maksud kondisi di pesan diagnostik.
Perilaku Saat assert Gagal #
Bila kondisi assert bernilai salah, macro menulis satu baris diagnostik ke standard error yang memuat teks ekspresi, nama file sumber (__FILE__), nomor baris (__LINE__), dan nama function pengapit (__func__); setelah itu std::abort() menghentikan program. Mari kita buat pemanggil yang melanggar prekondisi nomorPertama — vector kosong:
#include <cassert>
#include <iostream>
#include <vector>
int nomorPertama(const std::vector<int>& antrean) {
assert(!antrean.empty() && "pemanggil wajib memeriksa antrean dulu");
return antrean[0];
}
int main() {
std::vector<int> kosong;
std::cout << nomorPertama(kosong); // prekondisi dilanggar
return 0;
}Pesan yang dicetak ke standard error saat program itu dijalankan dalam kondisi gagal (eksekusi nyata di lingkungan uji; nama program di awal baris mengikuti nama binary dan nomor baris mengikuti posisi assert di file — program berakhir dengan sinyal SIGABRT, exit code 134):
assert-gagal: assert-gagal.cpp:6: int nomorPertama(const std::vector<int>&): Assertion `!antrean.empty() && "pemanggil wajib memeriksa antrean dulu"' failed.Pesan ini bisa dibaca seperti alamat: baris 6 di assert-gagal.cpp, function nomorPertama, beserta ekspresi yang gagal. Tidak ada catch yang bisa mencegat jalur ini — std::abort() menghentikan program langsung, mirip pelanggaran noexcept pada chapter sebelumnya.
NDEBUG: Build Debug dan Build Release #
Perilaku assert dikendalikan satu macro bernama NDEBUG, yang tidak didefinisikan oleh pustaka standar. Aturannya persis: bila NDEBUG terdefinisi pada titik <cassert> disertakan, macro assert melebar menjadi ((void)0) — tidak ada pemeriksaan, dan argumennya tidak dievaluasi sama sekali. Kompilasi tanpa -DNDEBUG lazim disebut build debug; kompilasi dengan -DNDEBUG (biasanya bersama -O2) disebut build release, topik yang dibedah lebih jauh pada Section Performa dan Optimasi. Program berikut membuktikan perbedaannya dengan satu penghitung global:
#include <cassert>
#include <iostream>
int evaluasi = 0;
int sisaStok(int stok, int terjual) {
++evaluasi; // penanda: apakah argumen assert dieksekusi?
return stok - terjual;
}
int main() {
assert(sisaStok(10, 4) == 6); // keadaan benar: seharusnya tidak ada efek
std::cout << "evaluasi setelah assert: " << evaluasi << "\n";
std::cout << "sisa stok: " << sisaStok(10, 4) << "\n";
std::cout << "evaluasi setelah pemanggilan: " << evaluasi << "\n";
return 0;
}Output yang Dihasilkan (kompilasi debug, tanpa -DNDEBUG):
evaluasi setelah assert: 1
sisa stok: 6
evaluasi setelah pemanggilan: 2Sekarang kompilasi file yang sama sebagai build release dengan flag -DNDEBUG:
g++ -std=c++23 -Wall -Wextra -DNDEBUG ndebug-demo.cpp -o ndebug-release && ./ndebug-releaseOutput yang Dihasilkan:
evaluasi setelah assert: 0
sisa stok: 6
evaluasi setelah pemanggilan: 1Angka 0 pada build release adalah bukti nyata: argumen assert tidak dievaluasi sama sekali, sehingga pemanggilan sisaStok(10, 4) di dalam assert tidak pernah terjadi. Pada build debug, macro mengevaluasi argumen di baris 12 sehingga penghitung global evaluasi naik menjadi 1, lalu pemanggilan sungguhan di baris 15 menaikkannya lagi. Karena argumen bisa saja tidak dieksekusi, peganglah satu praktik: buat setiap argumen assert bebas efek samping, seperti pada assert-demo.cpp yang outputnya identik di kedua build.
static_assert: Kontrak yang Diperiksa Compiler #
Chapter Compile-time vs Runtime Errors sudah memperkenalkan static_assert untuk asumsi ukuran tipe. Di sini kita melengkapinya dengan tiga hal: varian tanpa pesan yang sah sejak C++17, lokasi penulisan yang fleksibel (scope namespace, class body, dan block scope), serta kombinasi dengan type traits. Kondisinya harus constant expression: bila benar, deklarasi tidak berpengaruh apa pun; bila salah, kompilasi ditolak. Pemeriksaan ini aktif di semua build dan tanpa biaya runtime karena tidak ada kode yang dihasilkan. Sebagai pengingat singkat, type traits menanyakan sifat tipe saat compile time: std::is_integral_v<T> bernilai true bila T tipe integral seperti int atau long, dan akhiran _v adalah singkatan anggota value sejak C++17.
#include <iostream>
#include <type_traits>
// Kontrak di scope namespace: asumsi platform diperiksa compiler.
static_assert(sizeof(long long) >= sizeof(int), "asumsi ukuran tipe tidak terpenuhi");
// Tanpa pesan — sah sejak C++17; compiler membuatkan pesan default.
static_assert(sizeof(double) >= sizeof(float));
// Type traits dasar: std::is_integral_v<T> bernilai true bila T tipe integral.
static_assert(std::is_integral_v<int>, "skor harus ditampung tipe integral");
static_assert(!std::is_integral_v<double>, "double jelas bukan tipe integral");
class PencatatSkor {
public:
// Kontrak di dalam class body juga dievaluasi compiler.
static_assert(std::is_integral_v<int>, "tipe total harus integral");
void tambah(int poin) {
// Kontrak di block scope: dievaluasi saat compiler membaca function.
static_assert(sizeof(int) >= 4, "int minimal 4 byte");
total_ += poin;
}
int total() const { return total_; }
private:
int total_ = 0;
};
int main() {
PencatatSkor pencatat;
pencatat.tambah(90);
pencatat.tambah(15);
std::cout << "total skor: " << pencatat.total() << "\n";
std::cout << "semua kontrak compile time terpenuhi\n";
return 0;
}Output yang Dihasilkan:
total skor: 105
semua kontrak compile time terpenuhiBedah lokasi dan bentuknya:
- Baris 5 (dengan pesan): bentuk klasik
static_assert(kondisi, "pesan"). Pesan wajib berupa string literal dan tampil di pesan compiler bila kondisi gagal. - Baris 8 (tanpa pesan): sejak C++17, pesan boleh ditulis tidak ada; compiler membuatkan pesan default.
- Baris 11-12 (type traits):
std::is_integral_v<int>bernilaitrue, sedangkanstd::is_integral_v<double>bernilaifalse— tanda seru di depan membuat kontrak kedua tetap terpenuhi. - Baris 17 dan 21 (class body dan block scope):
static_assertboleh berdiri sebagai member declaration maupun di dalam function; keduanya dievaluasi saat compiler membaca deklarasi tersebut, tanpa menunggu program berjalan.
Nilai pesan khusus terasa saat kontrak gagal. Kedua file kecil berikut tidak berhasil dikompilasi, tetapi kualitas pesannya berbeda. Versi dengan pesan:
#include <type_traits>
static_assert(std::is_integral_v<double>, "skor harus ditampung tipe integral");
int main() { return 0; }kontrak-gagal.cpp:3:20: error: static assertion failed: skor harus ditampung tipe integral
3 | static_assert(std::is_integral_v<double>, "skor harus ditampung tipe integral");
| ~~~~~^~~~~~~~~~~~~~~~~~~~~Versi tanpa pesan:
static_assert(sizeof(double) == 4);
int main() { return 0; }kontrak-tanpa-pesan.cpp:1:30: error: static assertion failed
1 | static_assert(sizeof(double) == 4);
| ~~~~~~~~~~~~~~~^~~~
kontrak-tanpa-pesan.cpp:1:30: note: the comparison reduces to ‘(8 == 4)’Versi dengan pesan langsung menyatakan maksud kontraknya, hemat waktu siapa pun yang membaca error tersebut — tulis pesan setiap kali kontrak tidak cukup jelas dari ekspresinya sendiri.
Memilih Alat Pemeriksaan yang Tepat #
Tiga alat pelaporan kegagalan yang sudah kita pelajari sepanjang section ini ternyata punya wilayah kerja yang tidak saling menimpa:
| Alat | Waktu pemeriksaan | Kegagalan yang cocok | Perilaku saat gagal |
|---|---|---|---|
static_assert | compile time, semua build | kontrak tipe, asumsi platform, konstanta konfigurasi | kompilasi ditolak |
assert | runtime, hanya build tanpa NDEBUG | invariant internal dan prekondisi antar function internal | pesan di stderr, lalu std::abort() menghentikan program |
exception atau std::error_code | runtime, semua build | kegagalan yang diharapkan dan bisa ditangani pemanggil | dilaporkan sebagai nilai atau dilempar untuk ditangani |
Dari tabel itu, dua aturan praktis mengikat. Pertama, assert hanya untuk bug programmer: kondisi yang tidak mungkin benar selama kode kita benar, seperti prekondisi function internal atau invariant struktur data. Kedua, kegagalan yang diharapkan keberadaannya — berkas yang belum ada, input pengguna yang salah format, jaringan yang terputus — wajib ditangani dengan exception atau error code, bukan dengan assert yang dijamin hilang di build release.
Dengan demikian perangkat kita lengkap: static_assert menjaga kontrak di waktu kompilasi, assert menjaga invariant internal di build debug, dan exception menangani kegagalan yang bisa dipulihkan. Satu kebutuhan masih menggantung: kegagalan operasi sistem yang normal dan sering terjadi, di mana melempar exception terasa terlalu berat. Chapter berikutnya memperkenalkan jawaban pustaka standar untuk kebutuhan itu — std::error_code.
Referensi: cppreference — assert , cppreference — static_assert .