Concept (C++20)

Membatasi template secara terbaca sebagai pengganti SFINAE.

Daftar isi

Topik ini membahas concept dari C++20, yaitu cara menyatakan syarat yang harus dipenuhi parameter tipe secara eksplisit. Pembahasan dimulai dari pesan error template yang tidak dibatasi, dilanjutkan pendeklarasian concept dengan requires, perbandingan pesan error yang dihasilkan, dan concept bawaan pustaka standar. Fitur ini berjalan di atas standar C++20, sehingga contoh utamanya dikompilasi dengan opsi -std=c++23.

Template tanpa batasan dan errornya#

Template pada topik-topik sebelumnya tidak menyatakan syaratnya secara langsung, sehingga ketidakcocokan baru terlihat ketika badan template diinstansiasi. Fungsi jumlahElemen pada berkas berikut menuntut wadah.size(), tetapi tuntutan itu hanya tersirat di dalam badannya. Pemanggilan dengan int lolos dari tahap deduksi, lalu gagal di dalam badan template, dan pesan errornya muncul jauh dari pemanggilan yang menyebabkannya.

cpp tanpa-concept.cpp
// tidak-dijalankan (contoh error kompilasi; perintah build ada di blok bash di bawah)
#include <cstddef>

template <typename T>
std::size_t jumlahElemen(const T& wadah) {
    return wadah.size();
}

int main() {
    jumlahElemen(42);
}

Kompilasi berkas tersebut dengan flag default menghasilkan pesan error berikut.

bash
# kompilasi tanpa concept dengan flag default untuk melihat pesan errornya
g++ tanpa-concept.cpp -o /dev/null
text Keluaran
tanpa-concept.cpp: In instantiation of ‘std::size_t jumlahElemen(const T&) [with T = int; std::size_t = long unsigned int]’:
tanpa-concept.cpp:10:17:   required from here
   10 |     jumlahElemen(42);
      |     ~~~~~~~~~~~~^~~~
tanpa-concept.cpp:6:18: error: request for member ‘size’ in ‘wadah’, which is of non-class type ‘const int’
    6 |     return wadah.size();
      |            ~~~~~~^~~~

Pesan utamanya menunjuk baris return wadah.size() di dalam template, bukan baris jumlahElemen(42) yang menjadi penyebabnya. Pada template yang lebih besar dan tersusun berlapis, rangkaian catatan instantiation semacam ini memanjang, dan pembacanya harus menelusuri ke belakang untuk menemukan pemanggilan yang salah. Syarat yang sesungguhnya, yaitu T wajib memiliki method size, tidak pernah tertulis di bagian mana pun.

Mendeklarasikan concept#

Concept adalah ekspresi bernilai benar atau salah yang dinilai saat kompilasi untuk setiap tipe yang diujinya. Deklarasinya berupa template <typename T> concept Nama = ekspresi;, dan ekspresi yang paling lazim adalah requires, yaitu daftar persyaratan yang wajib terpenuhi oleh tipe yang diuji. Program berikut memakai concept bernama PunyaUkuran untuk membatasi fungsi laporkan, ditambah requires clause requires std::integral<T> yang memakai concept pustaka standar.

cpp main.cpp
// tidak-dijalankan (butuh -std=c++23; perintah build ada di bawah)
#include <concepts>
#include <cstddef>
#include <iostream>
#include <vector>

template <typename T>
concept PunyaUkuran = requires(const T& wadah) {
    { wadah.size() } -> std::integral;
};

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

template <typename T>
    requires std::integral<T>
T kaliDua(T nilai) {
    return nilai * 2;
}

int main() {
    std::vector<int> angka{10, 24, 7};
    laporkan(angka);

    std::cout << kaliDua(21) << "\n";
    std::cout << kaliDua(500L) << "\n";
    return 0;
}

Flag default sandbox ini adalah gnu++17, dan kata kunci concept belum tersedia pada standar itu. Kompilasi dilakukan dengan opsi standar yang lebih baru.

bash
# kompilasi dengan standar C++23
g++ -std=c++23 main.cpp -o main
# jalankan program
./main
text Keluaran
Jumlah elemen: 3
42
1000

Bentuk { wadah.size() } -> std::integral menyatakan dua hal sekaligus, yaitu ekspresi wadah.size() harus valid dan hasilnya harus memenuhi concept std::integral. std::vector<int> memenuhi persyaratannya, sehingga laporkan(angka) terinstansiasi, sedangkan kaliDua yang dibatasi std::integral menerima int dan long sesuai keluaran 42 dan 1000.

Dua bentuk pembatasan pada program di atas setara untuk fungsi template sederhana, yaitu parameter template yang dibatasi langsung dan requires clause yang ditulis setelah daftar parameter template. Bentuk ketiga, yaitu requires clause pada akhir deklarasi fungsi, dibutuhkan ketika syaratnya baru dapat dituliskan setelah tanda tangan fungsi lengkap, misalnya pada member function non-template di dalam class template. Ketiganya bermuara pada pemeriksaan yang sama, sehingga pemilihannya disesuaikan dengan letak yang paling alami dibaca. Pemilihan letaknya tidak memengaruhi pesan error yang dihasilkan.

Pesan error yang menunjuk penyebabnya#

Berkas berikut memuat fungsi jumlahElemen yang sama, tetapi kini dibatasi concept PunyaUkuran. Isinya tetap memanggil jumlahElemen(42) supaya kedua pesan error dapat dibandingkan secara adil.

cpp dengan-concept.cpp
// tidak-dijalankan (contoh error kompilasi; perintah build ada di blok bash di bawah)
#include <concepts>
#include <cstddef>

template <typename T>
concept PunyaUkuran = requires(const T& wadah) {
    { wadah.size() } -> std::integral;
};

template <PunyaUkuran T>
std::size_t jumlahElemen(const T& wadah) {
    return wadah.size();
}

int main() {
    jumlahElemen(42);
}
bash
# kompilasi dengan standar C++23 untuk melihat pesan error yang lebih jelas
g++ -std=c++23 dengan-concept.cpp -o /dev/null
text Keluaran
dengan-concept.cpp: In function ‘int main()’:
dengan-concept.cpp:16:17: error: no matching function for call to ‘jumlahElemen(int)’
   16 |     jumlahElemen(42);
      |     ~~~~~~~~~~~~^~~~
dengan-concept.cpp:11:13: note: candidate: ‘template<class T>  requires  PunyaUkuran<T> std::size_t jumlahElemen(const T&)’
   11 | std::size_t jumlahElemen(const T& wadah) {
      |             ^~~~~~~~~~~~
dengan-concept.cpp:11:13: note:   template argument deduction/substitution failed:
dengan-concept.cpp:11:13: note: constraints not satisfied
dengan-concept.cpp: In substitution of ‘template<class T>  requires  PunyaUkuran<T> std::size_t jumlahElemen(const T&) [with T = int]’:
dengan-concept.cpp:16:17:   required from here
   16 |     jumlahElemen(42);
      |     ~~~~~~~~~~~~^~~~
dengan-concept.cpp:6:9:   required for the satisfaction of ‘PunyaUkuran<T>’ [with T = int]
dengan-concept.cpp:6:23:   in requirements with ‘const T& wadah’ [with T = int]
dengan-concept.cpp:7:17: note: the required expression ‘wadah.size()’ is invalid
    7 |     { wadah.size() } -> std::integral;
      |       ~~~~~~~~~~^~
cc1plus: note: set ‘-fconcepts-diagnostics-depth=’ to at least 2 for more detail

Pesan error kini dimulai pada baris pemanggilan yang salah dengan teks no matching function for call to 'jumlahElemen(int)', disusul catatan constraints not satisfied yang menyebut nama conceptnya. Baris terakhir menunjuk persyaratan yang gagal, yaitu ekspresi wadah.size() pada deklarasi concept. Penyebabnya dan letak syaratnya sama-sama terlihat, sehingga perbaikannya dapat ditentukan tanpa menelusuri badan template.

Concept pustaka standar#

Header <concepts> menyediakan concept bawaan yang lazim dipakai, di antaranya std::integral untuk tipe bilangan bulat, std::floating_point untuk tipe pecahan, std::same_as untuk kesamaan dua tipe, dan std::convertible_to untuk kemampuan konversi antar tipe. Concept juga dapat digabung dengan && dan ||, misalnya std::integral<T> || std::floating_point<T>, dan hasil gabungannya tetap sebuah concept.

Sebelum C++20, pembatasan template dilakukan dengan SFINAE, yaitu singkatan dari Substitution Failure Is Not An Error. Sesuai namanya, instansiasi yang gagal pada tahap substitusi hanya dilewati alih-alih dilaporkan sebagai error. Teknik itu bekerja tetapi tulisannya bertele-tele dan pesan errornya tidak lebih baik. Concept menggantikan pola semacam itu dengan deklarasi yang dapat dibaca, dan syaratnya juga dapat diuji langsung melalui static_assert tanpa membuat template apa pun.

cpp Contoh: menguji concept dengan static_assert
// tidak-dijalankan (butuh -std=c++23; potongan konseptual)
static_assert(std::integral<int>);
static_assert(!std::integral<double>);
static_assert(std::convertible_to<int, double>);

Concept bawaan menjadi bahan bangunan bagi concept yang dibuat sendiri. Concept Angka di bawah menggabungkan std::integral dan std::floating_point dengan ||, sehingga satu nama mencakup seluruh kelompok tipe bilangan. Setelah dideklarasikan, concept itu dipakai pada posisi typename biasanya berada, dan definisinya cukup diperbarui di satu tempat ketika cakupan kelompoknya berubah.

cpp angka-concept.cpp
// tidak-dijalankan (butuh -std=c++23; perintah build ada di bawah)
#include <concepts>
#include <iostream>

// concept yang disusun dari dua concept pustaka standar
template <typename T>
concept Angka = std::integral<T> || std::floating_point<T>;

template <Angka T>
T kaliTiga(T nilai) {
    return nilai * 3;
}

int main() {
    std::cout << kaliTiga(7) << "\n";
    std::cout << kaliTiga(2.5) << "\n";
    return 0;
}
bash
# kompilasi dengan standar C++23
g++ -std=c++23 angka-concept.cpp -o angka
# jalankan program
./angka
text Keluaran
21
7.5

Pemanggilan kaliTiga(7) menginstansiasi versi int dan menghasilkan 21, sedangkan kaliTiga(2.5) memakai versi double dan menghasilkan 7.5. Pemanggilan dengan std::string akan ditolak pada baris pemanggilannya karena Angka tidak terpenuhi. Pesan yang muncul menunjuk pemanggilan dan persyaratan yang gagal, sebagaimana pola pesan error pada bagian sebelumnya.

Kesalahan umum#

Empat kekeliruan berikut paling sering muncul saat mulai menulis concept.

  • Mengompilasi kode concept dengan flag default. Kata kunci concept dan header <concepts> menuntut C++20, sehingga compiler melaporkan concept does not name a type. Tambahkan -std=c++20 atau -std=c++23 pada perintah build.
  • Menuliskan hasil persyaratan dengan trait biasa, misalnya -> std::is_integral_v<T>. Bentuk setelah tanda panah wajib berupa concept, sedangkan is_integral_v hanyalah variabel konstanta. Gunakan std::integral dari header <concepts>.
  • Mengira concept menjalankan pemeriksaan saat program berjalan. Concept dinilai sepenuhnya saat kompilasi, sehingga tidak ada biaya tambahan pada program yang berjalan, tetapi juga tidak ada pelindung terhadap tipe yang dikirim dari bagian yang sudah terkompilasi sebelumnya.
  • Menuliskan requires expression dengan objek yang bisa diubah padahal pemakaiannya melalui referensi const. Persyaratan requires(T wadah) { wadah.size(); } hanya membuktikan size tersedia pada objek non-const, sedangkan pemanggilan nyata pada objek const tetap bisa gagal di dalam badan template. Uji persyaratan dengan const T& agar hasil pemeriksaannya sesuai pemakaian sesungguhnya.

Ringkasan#

  • Concept menyatakan syarat parameter tipe secara eksplisit dan dinilai saat kompilasi, sedangkan requires menuliskan daftar persyaratannya dalam bentuk ekspresi.
  • Template yang dibatasi concept melaporkan error pada baris pemanggilan dan menyebut persyaratan yang gagal, tidak seperti template tanpa batasan yang menunjuk badan template.
  • Header <concepts> menyediakan concept bawaan seperti std::integral dan std::convertible_to, dan concept menggantikan pola SFINAE yang bertele-tele.
  • Concept dapat disusun dari concept lain dengan && dan ||, dan syaratnya dapat dituliskan pada parameter template maupun pada akhir deklarasi fungsi.

Referensi#