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:

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

text
nomor pertama: 101
total: 306
semua assert lolos, program berlanjut normal

Mari kita telaah poin-poin pentingnya:

  1. Baris 1 dan 8 (#include <cassert>, assert(!antrean.empty() && "...")): header wajib agar macro assert tersedia, lalu pemeriksaan prekondisi di awal function; bila kondisi benar, program berlanjut tanpa efek tambahan.
  2. Baris 9 (return antrean[0];): akses elemen pertama sah karena prekondisi baru saja diperiksa — assert mendokumentasikan kontrak itu bagi pemanggil internal.
  3. Baris 19-20 (perhitungan dan assert(total == 306)): assert juga bisa menjaga hasil perhitungan — pengujian kecil yang menumpang di kode produksi.
  4. 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:

cpp Contoh: prekondisi dilanggar
#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):

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

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

text
evaluasi setelah assert: 1
sisa stok: 6
evaluasi setelah pemanggilan: 2

Sekarang kompilasi file yang sama sebagai build release dengan flag -DNDEBUG:

bash
g++ -std=c++23 -Wall -Wextra -DNDEBUG ndebug-demo.cpp -o ndebug-release && ./ndebug-release

Output yang Dihasilkan:

text
evaluasi setelah assert: 0
sisa stok: 6
evaluasi setelah pemanggilan: 1

Angka 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.

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

text
total skor: 105
semua kontrak compile time terpenuhi

Bedah lokasi dan bentuknya:

  1. Baris 5 (dengan pesan): bentuk klasik static_assert(kondisi, "pesan"). Pesan wajib berupa string literal dan tampil di pesan compiler bila kondisi gagal.
  2. Baris 8 (tanpa pesan): sejak C++17, pesan boleh ditulis tidak ada; compiler membuatkan pesan default.
  3. Baris 11-12 (type traits): std::is_integral_v<int> bernilai true, sedangkan std::is_integral_v<double> bernilai false — tanda seru di depan membuat kontrak kedua tetap terpenuhi.
  4. Baris 17 dan 21 (class body dan block scope): static_assert boleh 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:

cpp Contoh: static_assert gagal dengan pesan
#include <type_traits>

static_assert(std::is_integral_v<double>, "skor harus ditampung tipe integral");

int main() { return 0; }
text
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:

cpp Contoh: static_assert gagal tanpa pesan
static_assert(sizeof(double) == 4);

int main() { return 0; }
text
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:

AlatWaktu pemeriksaanKegagalan yang cocokPerilaku saat gagal
static_assertcompile time, semua buildkontrak tipe, asumsi platform, konstanta konfigurasikompilasi ditolak
assertruntime, hanya build tanpa NDEBUGinvariant internal dan prekondisi antar function internalpesan di stderr, lalu std::abort() menghentikan program
exception atau std::error_coderuntime, semua buildkegagalan yang diharapkan dan bisa ditangani pemanggildilaporkan 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 .