Chapter sebelumnya menutup dengan janji: concept buatan sendiri yang mengecek keberadaan method dan operator ditulis dengan satu alat bernama requires expression. Alat inilah topik kita sekarang. requires expression adalah ekspresi boolean tentang tipe — kita mendaftar operasi yang harus sah, seperti t.size() atau a + b, lalu compiler memeriksa semuanya pada saat kompilasi tanpa menjalankan satu baris pun. Hasilnya bisa diberi nama menjadi concept, dipakai seketika di dalam if constexpr, atau ditempel langsung ke template lewat pola requires requires. Setelah chapter ini, kita tidak lagi bergantung pada concept baku untuk mengekspresikan kendala.

Anatomi requires Expression #

Bentuk dasarnya seperti ini:

cpp
requires (daftar-parameter) {
    // daftar requirement, satu per baris
}

Compiler membuat “dunia hipotesis” di dalamnya: setiap parameter di daftar menjadi object fiktif, lalu semua requirement diuji satu per satu. Bila semuanya sah, hasilnya true; satu saja gagal, hasilnya false — dan yang terpenting, kegagalan itu bukan error, hanya jawaban bernilai false. Ada empat jenis requirement yang bisa kita daftarkan:

JenisBentukYang diperiksa
simplet.size();ekspresi sah untuk dikompilasi
typetypename T::value_type;tipe nested bernama itu ada
compound{ t.size() } -> std::convertible_to<std::size_t>;ekspresi sah dan hasilnya memenuhi concept
nestedrequires std::default_initializable<T>;concept lain terpenuhi untuk T

Contoh berikut memakai requires expression di dua peran: sebagai badan concept bernama, dan sebagai kendala seketika di dalam if constexpr:

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

// Concept: T punya method size() yang hasilnya bilangan asli
template <typename T>
concept PunyaUkuran = requires(T t) {
    t.size();
    { t.size() } -> std::convertible_to<std::size_t>;
};

// Concept: dua nilai bertipe T bisa dijumlah
template <typename T>
concept BisaDitambah = requires(T a, T b) {
    a + b;
};

template <PunyaUkuran T>
void laporan(const T& wadah) {
    std::cout << "ukuran: " << wadah.size() << "\n";
}

template <BisaDitambah T>
T gabung(const T& a, const T& b) {
    return a + b;
}

// requires expression di dalam if constexpr: pilih cabang berdasar kemampuan
template <typename T>
void uji(const T& nilai) {
    if constexpr (requires { nilai.size(); }) {
        std::cout << "punya size(): " << nilai.size() << "\n";
    } else {
        std::cout << "tidak punya size()\n";
    }
}

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

    std::cout << "gabung angka : " << gabung(10, 32) << "\n";
    std::cout << "gabung string: " << gabung(std::string("bulan"), std::string("sabit")) << "\n";

    uji(std::vector<int>{9});
    uji(42);

    std::cout << std::boolalpha
              << "int memenuhi PunyaUkuran? " << PunyaUkuran<int> << "\n"
              << "vector memenuhi PunyaUkuran? " << PunyaUkuran<std::vector<int>> << "\n";

    return 0;
}

Output yang Dihasilkan:

text
ukuran: 3
ukuran: 4
gabung angka : 42
gabung string: bulansabit
punya size(): 1
tidak punya size()
int memenuhi PunyaUkuran? false
vector memenuhi PunyaUkuran? true

Bedah baris demi baris:

  1. Baris 8 (concept PunyaUkuran = requires(T t) { ... }): requires expression dengan satu parameter fiktif t bertipe T. Object t tidak pernah dibuat — ia hanya alat peraga bagi compiler untuk mengetes ekspresi.
  2. Baris 9 (t.size();): simple requirement. Cukup meminta ekspresi sah; hasil kembaliannya tidak dituntut apa pun.
  3. Baris 10 ({ t.size() } -> std::convertible_to<std::size_t>;): compound requirement. Selain ekspresi harus sah, tipe hasilnya juga wajib memenuhi concept std::convertible_to<std::size_t> — jadi method yang mengembalikan sembarang tipe tidak otomatis lolos.
  4. Baris 15-16 (a + b;): concept BisaDitambah mengecek operator + antara dua nilai bertipe T. gabung(10, 32) menghasilkan 42, sedangkan gabung dua string menyambung keduanya — satu template, perilaku sesuai tipe.
  5. Baris 32 (if constexpr (requires { nilai.size(); })): requires expression dipakai ad-hoc, tanpa nama, langsung sebagai kondisi. uji menghasilkan instansiasi berbeda untuk vector dan untuk int — cabang yang tidak terpilih tidak dikompilasi (materi penuhnya di chapter berikutnya).
  6. Baris 49-51: requires expression yang sudah dibungkus concept ternyata bisa dipakai sebagai nilai boolean biasa. PunyaUkuran<int> bernilai false karena int tidak punya method size(); PunyaUkuran<std::vector<int>> bernilai true.

Dari requires Expression ke Concept #

Concept dan requires expression adalah dua lapisan yang saling melengkapi. requires expression menjawab “apakah tipe ini mampu melakukan ini?” untuk satu tipe; concept memberi nama pada pertanyaan itu agar bisa dipakai ulang di banyak tempat. Secara sintaks, concept Nama = requires(...) { ... }; hanyalah penugasan ekspresi ke sebuah nama — baris 8 pada requires.cpp di atas adalah contoh utuhnya.

Karena hasilnya boolean, concept buatan kita juga bisa digabung seperti logika biasa. concept BisaDiukurDanDitambah = PunyaUkuran<T> && BisaDitambah<T>; sah dan terbaca seperti kalimat. Anda juga boleh mencampur concept baku dan buatan dalam satu gabungan, misalnya std::integral<T> || PunyaUkuran<T>. Pola kombinasi inilah yang membuat kendala panjang tetap terorganisasi: pecah menjadi concept kecil yang teruji, lalu susun.

requires requires: Kendala Tanpa Nama #

Ada kalanya kita butuh kendala sekali pakai tetapi tidak mau mendefinisikan concept. Untuk itulah ada pola requires requires: requires clause (kendala pada template) yang isinya requires expression (pemeriksaan kemampuan). Contoh berikut memakai pola itu sekaligus memamerkan keempat jenis requirement dalam satu concept:

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

// Concept dengan empat jenis requirement sekaligus
template <typename T>
concept Wadah = requires(T t) {
    // 1. simple requirement: ekspresi harus sah (hasilnya tidak dituntut)
    t.clear();

    // 2. type requirement: tipe nested bernama value_type harus ada
    typename T::value_type;

    // 3. compound requirement: ekspresi sah DAN hasilnya memenuhi concept
    { t.size() } -> std::convertible_to<std::size_t>;

    // 4. nested requirement: concept lain harus terpenuhi untuk T
    requires std::default_initializable<T>;
};

// requires requires: kendala ad-hoc tanpa memberi nama concept
template <typename T>
requires requires(T t) { t.size(); }
std::size_t jumlahElemen(const T& wadah) {
    return wadah.size();
}

// Overload tanpa kendala: dipakai hanya bila kandidat terkendali tidak cocok
template <typename T>
std::size_t jumlahElemen(const T&) {
    return 1;
}

int main() {
    static_assert(Wadah<std::vector<int>>);
    static_assert(Wadah<std::string>);

    std::cout << "vector        : " << jumlahElemen(std::vector<int>{1, 2, 3}) << "\n";
    std::cout << "string        : " << jumlahElemen(std::string("kopi")) << "\n";
    std::cout << "angka tunggal : " << jumlahElemen(7) << "\n";

    return 0;
}

Output yang Dihasilkan:

text
vector        : 3
string        : 4
angka tunggal : 1

Bedah baris demi baris:

  1. Baris 8-20 (concept Wadah): satu requires expression berisi keempat jenis requirement sekaligus — setiap jenisnya menutup celah yang tidak ditangkap jenis lain.
  2. Baris 13 (typename T::value_type;): type requirement. std::vector<int> dan std::string sama-sama punya tipe nested value_type; int tidak, sehingga Wadah<int> langsung false tanpa perlu menilai requirement lain.
  3. Baris 16: compound requirement menuntut t.size() sah dan hasilnya bisa dikonversi ke std::size_t — versi ketat dari simple requirement pada chapter-concept kita sebelumnya.
  4. Baris 19 (requires std::default_initializable<T>;): nested requirement. Kata requires di dalam badan menandai “kendala lain diperiksa di sini” — tanpa itu, std::default_initializable<T> hanya ekspresi yang dinilai, bukan syarat yang diuji.
  5. Baris 24 (requires requires(T t) { t.size(); }): pola dua kata kunci. requires pertama adalah requires clause milik template; requires kedua membuka requires expression sebagai isinya. Keduanya punya tugas berbeda, meski namanya sama.
  6. Baris 30-33: overload tanpa kendala menjadi jalan keluar bila kandidat terkendali tidak cocok. Untuk jumlahElemen(7), versi terkendali ditolak (int tidak punya size()), sehingga compiler jatuh ke versi ini dan mengembalikan 1.
  7. Baris 36-41: static_assert membuktikan Wadah benar untuk vector dan string, lalu tiga pemanggilan menunjukkan pemilihan overload: vector menghasilkan 3, string 4, sedangkan angka tunggal lewat jalur fallback.

Pesan Error yang Menunjuk Requirement #

Bagaimana bila kendala dilanggar? Compiler modern tidak berhenti di “tidak cocok” — ia menunjuk requirement mana yang gagal. Aktifkan static_assert(Wadah<int>, "int bukan Wadah"); pada baris 36:

cpp static_assert(Wadah<int>) (tidak dikompilasi)
#include <concepts>

template <typename T>
concept Wadah = requires(T t) {
    t.clear();
    typename T::value_type;
    { t.size() } -> std::convertible_to<std::size_t>;
    requires std::default_initializable<T>;
};

static_assert(Wadah<int>, "int bukan Wadah");
int main() { return 0; }

Potongan pesan nyata dari g++ 14.2 (dipersingkat):

text
wadah-int.cpp:13:15: error: static assertion failed: int bukan Wadah
   13 | static_assert(Wadah<int>, "int bukan Wadah");
      |               ^~~~~~~~~~
wadah-int.cpp:13:15: note: constraints not satisfied
wadah-int.cpp:6:17:   in requirements with 'T t' [with T = int]
wadah-int.cpp:7:12: note: the required expression 't.clear()' is invalid
    7 |     t.clear();
      |     ~~~~~~~^~
wadah-int.cpp:8:14: note: the required type 'typename T::value_type' is invalid
    8 |     typename T::value_type;
      |     ~~~~~~~~~^~~~~~~~~~~~~~
wadah-int.cpp:9:13: note: the required expression 't.size()' is invalid
    9 |     { t.size() } -> std::convertible_to<std::size_t>;
      |       ~~~~~~^~

Compiler menelusuri daftar requirement satu per satu dan melaporkan setiap yang invalid: t.clear() tidak sah, value_type tidak ada, t.size() tidak sah. Pembaca pesan bisa langsung melihat kontrak yang dituntut, bukan menebak-nebak dari “no matching function”. Catatan kecil: baris terakhir pesan menyarankan -fconcepts-diagnostics-depth=2 bila kita ingin compiler mencantumkan detail lebih dalam lagi.

Sekarang semua alat kendala sudah lengkap: requires expression untuk menguji kemampuan, concept untuk memberi nama, dan requires clause untuk menempelkannya. Yang belum kita bahas adalah menegakkan aturan dengan pesan sendiri dan memilih perilaku berbeda per tipe pada saat kompilasi — dua tugas itu menjadi topik Chapter Compile-time Constraints.

Referensi resmi: cppreference — requires expression dan cppreference — Constraints and concepts .