Chapter sebelumnya memberi kita alat pengujian: requires expression menjawab ya atau tidak tentang kemampuan sebuah tipe. Chapter ini melangkah ke alat penegakan dan alat percabangan. static_assert menghentikan kompilasi dengan pesan yang kita tulis sendiri bila sebuah aturan dilanggar; kendala pada class template memastikan type yang salah tidak pernah sampai ke constructor; sedangkan if constexpr menyuruh compiler memilih satu cabang kode pada saat kompilasi sehingga satu template bisa bersikap berbeda untuk tipe yang berbeda. Ketiganya menjadi alasan utama mengapa pola SFINAE manual yang gelap perlahan pensiun.

static_assert: Menolak dengan Pesan yang Kita Tulis #

static_assert(kondisi, "pesan") adalah pemeriksaan yang hidup sepenuhnya di saat kompilasi. Kondisinya wajib berupa constant expression — sesuatu yang bisa dinilai compiler tanpa menjalankan program, misalnya trait seperti std::is_integral_v<T> atau concept seperti std::integral<T>. Bila kondisi false, compiler berhenti dan mencetak pesan kita apa adanya; bila true, tidak ada jejak sama sekali di program akhir. Kita sudah sekali memakainya sebagai pemeriksa tetap di Chapter Type Traits — sekarang kita letakkan di tempat yang jauh lebih berguna: di dalam template.

Ketika static_assert berada di dalam class template, penilaiannya menunggu sampai template diinstansiasi. Inilah pembedanya: template yang tidak pernah dipakai tidak pernah memicu pemeriksaan, sedangkan satu kali instansiasi dengan tipe yang salah langsung terdeteksi. Contoh berikut membandingkan dua cara menjaga class template:

cpp
#include <concepts>
#include <iostream>
#include <string>
#include <type_traits>

// static_assert pada scope global: diperiksa segera saat kompilasi
static_assert(std::is_integral_v<int>, "int wajib integral — asumsi pustaka kita");

// static_assert di dalam class template: diperiksa saat template diinstansiasi
template <typename T>
class Kotak {
    static_assert(std::is_integral_v<T>,
                  "Kotak<T>: T harus tipe integral (int, long, char, ...)");
public:
    explicit Kotak(T isi) : isi_{isi} {}
    T isi() const { return isi_; }
private:
    T isi_;
};

// Cara modern: kendala lewat requires clause pada class template
template <typename T>
requires std::floating_point<T>
class RataRata {
public:
    explicit RataRata(T dasar) : dasar_{dasar} {}
    T tambah(T nilai) { dasar_ += nilai; return dasar_; }
private:
    T dasar_;
};

int main() {
    Kotak<int> angka{42};
    Kotak<char> huruf{'A'};
    std::cout << "Kotak<int> : " << angka.isi() << "\n";
    std::cout << "Kotak<char>: " << huruf.isi() << "\n";

    RataRata<double> rata{1.5};
    std::cout << "RataRata   : " << rata.tambah(2.25) << "\n";

    // Kotak<std::string> teks{"halo"};
    //   error: static_assert gagal — "Kotak<T>: T harus tipe integral ..."

    return 0;
}

Output yang Dihasilkan:

text
Kotak<int> : 42
Kotak<char>: A
RataRata   : 3.75

Bedah baris demi baris:

  1. Baris 7: static_assert pada scope global dinilai segera, bahkan sebelum template mana pun diinstansiasi. Bila standar bahasa berubah sehingga int bukan integral lagi, program ini menolak dikompilasi di baris pertama pelanggarannya.
  2. Baris 12-13: pemeriksaan yang sama kini menunggu instansiasi. Kotak<int> dan Kotak<char> lolos karena keduanya integral; template Kotak<double> — sekalipun tidak dipanggil di sini — akan gagal tepat di baris ini.
  3. Baris 34-36: Kotak<char> menyimpan 'A' dan mencetaknya sebagai karakter, bukan angka 65 — tipe aslinya tetap char, hanya penjaga pintunya yang berbeda.
  4. Baris 22-23 (requires std::floating_point<T>): kendala kedua ini memakai requires clause, alat dari Chapter Concepts, langsung pada class template. RataRata<int> tidak ditolak dengan pesan kita, melainkan dicoret dari daftar kandidat seperti concept pada umumnya.
  5. Baris 38-39: 1.5 + 2.25 = 3.75 — hasil RataRata hanya masuk akal untuk tipe floating point, dan kendala di baris 23 itulah yang menjaminnya.

Perbedaan kedua alat ini penting untuk dipilih dengan sadar. static_assert adalah aturan keras dengan pesan buatan sendiri: template gagal instansiasi dan compiler menunjuk baris kita. Requires clause adalah aturan seleksi: kandidat yang melanggar hanya dicoret, dan bila ada kandidat lain yang cocok, program tetap jalan tanpa error. Aktifkan baris 41 di atas (Kotak<std::string> teks{"halo"};) dan compiler membalas:

text
kotak-str.cpp: In instantiation of 'class Kotak<std::__cxx11::basic_string<char> >':
kotak-str.cpp:15:33:   required from here
kotak-str.cpp:6:24: error: static assertion failed: Kotak<T>: T harus tipe integral (int, long, char, ...)
kotak-str.cpp:6:24: note: 'std::is_integral_v<std::__cxx11::basic_string<char> >' evaluates to false

Pesan ini menyebut tipe pelanggar secara lengkap (std::string dalam nama aslinya basic_string<char>) lalu menutup dengan alasan yang kita tulis sendiri. Siapa pun yang salah memakai Kotak akan mengerti dalam sepuluh detik, tanpa membaca dokumentasi.

if constexpr: Percabangan pada Saat Kompilasi #

static_assert dan requires clause menjaga pintu masuk; if constexpr mengatur isi rumah. Sintaksnya seperti if biasa dengan tambahan kata constexpr, tetapi kondisinya wajib konstanta dan keputusannya diambil compiler — bukan saat program berjalan. Konsekuensi terpentingnya muncul di dalam template: cabang yang tidak terpilih (discarded statement) tidak diinstansiasi, sehingga kode yang tidak sah untuk tipe tertentu boleh berada di sana tanpa menimbulkan error.

Contoh berikut menyusun satu fungsi dengan tiga kepribadian: perilaku angka, perilaku container, dan pesan penolakan yang ramah untuk sisanya:

cpp
#include <concepts>
#include <iostream>
#include <string>
#include <type_traits>
#include <vector>

// Tipe yang tidak pernah bernilai true: bahan pesan error yang ramah
template <typename T>
struct SelaluSalah : std::false_type {};

template <typename T>
void periksa(const T& nilai) {
    if constexpr (std::integral<T> || std::floating_point<T>) {
        std::cout << "angka    : " << nilai
                  << " (dua kali lipat = " << nilai * 2 << ")\n";
    } else if constexpr (requires { nilai.size(); }) {
        std::cout << "kontainer: " << nilai.size() << " elemen\n";
    } else {
        static_assert(SelaluSalah<T>::value,
                      "periksa(): tipe ini belum didukung");
    }
}

int main() {
    periksa(21);
    periksa(3.5);
    periksa(std::string("lima"));
    periksa(std::vector<int>{1, 2, 3});

    // struct Titik { int x, y; };
    // periksa(Titik{1, 2});  // error: "periksa(): tipe ini belum didukung"

    return 0;
}

Output yang Dihasilkan:

text
angka    : 21 (dua kali lipat = 42)
angka    : 3.5 (dua kali lipat = 7)
kontainer: 4 elemen
kontainer: 3 elemen

Bedah baris demi baris:

  1. Baris 8-9 (struct SelaluSalah : std::false_type {}): template struct yang mewarisi std::false_type sehingga nilai ::value-nya selalu false. Terasa aneh menyiapkan “kebenaran yang selalu salah”, tetapi kegunaannya muncul di baris 19 — ia membuat pesan error baru aktif hanya untuk tipe yang sampai ke cabang ini.
  2. Baris 13-15: cabang pertama ditulis seolah nilai bisa dikalikan dua. Untuk periksa(21) dan periksa(3.5) memang begitu; untuk std::string, penjumlahan nilai * 2 sebenarnya tidak sah — tetapi tidak pernah dikompilasi karena cabang ini diabaikan saat T bukan angka.
  3. Baris 16-17: kondisi kedua memakai requires expression ad-hoc dari Chapter requires Expressions: “apakah nilai punya method size()?”. std::string("lima") punya 4 karakter dan std::vector<int> berisi 3 elemen — keduanya jatuh ke cabang ini dengan hasil berbeda.
  4. Baris 19-20: static_assert di dalam cabang terakhir. Bila template diinstansiasi dengan tipe yang bukan angka dan tidak punya size() — misalnya struct Titik pada baris 31 — compiler menampilkan pesan ramah kita, bukan lima halaman error tentang operator yang hilang.
  5. Baris 25-28: empat pemanggilan menghasilkan tiga instansiasi berbeda: periksa<int> dan periksa<double> hanya berisi kode angka, sedangkan versi string dan vector masing-masing hanya berisi kode kontainer. Satu fungsi di kode sumber, tiga fungsi di binary.

Menggantikan SFINAE Manual #

Sebelum C++20, dua kebutuhan di atas ditangani dengan trik yang disebut SFINAE — Substitution Failure Is Not An Error. Chapter Type Traits menunjukkan contoh termudahnya dengan enable_if: return type fungsi sengaja dibuat “hilang” bila kondisi tidak terpenuhi, sehingga kandidat fungsi tersingkir diam-diam. Teknik itu bekerja, tetapi menyembunyikan maksud di balik manipulasi tipe dan menghasilkan pesan error yang berhenti di “no matching function”. Bandingkan langsung:

cpp SFINAE manual vs kendala modern (keduanya setara)
// Cara lama: enable_if menyembunyikan maksud di return type
template <typename T>
std::enable_if_t<std::is_integral_v<T>, T> gandakan(T nilai) {
    return nilai * 2;
}

// Cara modern: maksud ditulis di muka, pesan error menjelaskan diri
template <typename T>
requires std::integral<T>
T gandakan(T nilai) {
    return nilai * 2;
}

Kedua versi menolak gandakan(2.5) dengan cara yang sama secara mekanis — hanya satu yang diinstansiasi. Bedanya pada keterbacaan dan pada pengalaman gagal: versi modern menyatakan niat, ikut serta dalam pemilihan overload, dan menghasilkan pesan bertingkat yang sudah kita buktikan pada Chapter Concepts. Tabel kecil ini merangkum pergantian alat:

KebutuhanAlat lamaAlat modern
menolak tipe tertentuenable_if + SFINAErequires clause atau concept
perilaku berbeda per tipespesialisasi template berlapisif constexpr
menegakkan asumsi(tidak ada alat khusus)static_assert dengan pesan
mengetes kemampuan tipetrait + void_trequires expression

Pola void_t pada baris terakhir tabel adalah trik populer era C++14 untuk mengecek keberadaan member lewat kegagalan substitusi — kabar baiknya, setiap tempat di mana void_t dulu berperan kini memiliki pengganti requires expression yang jujur tentang maksudnya.

Dengan kendala, pemeriksaan, dan percabangan compile time di tangan, kotak alat pemrograman generik kita resmi lengkap. Yang tersisa adalah merakitnya menjadi pola-pola desain yang sudah mapan di komunitas — policy-based design, tag dispatch, CRTP — dan itulah isi Chapter Generic Programming Patterns.

Referensi resmi: cppreference — static_assert dan cppreference — if statement .