Tiga belas chapter terakhir membangun kotak alat: template, pack, fold, trait, concept, requires expression, static_assert, dan if constexpr. Chapter ini merakitnya menjadi pola-pola desain yang sudah mapan di komunitas C++ — policy-based design, tag dispatch, CRTP, dan sekilas type erasure. Masing-masing adalah resep untuk masalah nyata: perilaku yang dikomposisikan saat kompilasi, keputusan berdasarkan label tipe, pewarisan tanpa biaya virtual, dan antarmuka yang menyembunyikan tipe. Setelah itu kita tutup dengan pertanyaan paling praktis: kapan memilih generik, kapan memilih OOP. Chapter ini sekaligus menutup Section Generic Programming dan Templates.
Policy-Based Design: Perilaku sebagai Parameter Template #
Pola pertama menyusun perilaku dari keping kecil bernama policy — class kecil yang masing-masing menangani satu aspek, lalu dirangkai sebagai parameter template. STL memakai pola ini lebih dulu daripada kita sadari: std::vector<int, std::allocator<int>> punya dua parameter template, dan parameter kedua itulah policy yang mengatur cara memesan memori. Prinsipnya sederhana: bagian yang berubah-ubah dijadikan parameter, bagian yang tetap ditulis sekali di class host.
Contoh kita menyusun dua policy — cara mencetak dan pemisah antar item — menjadi satu class Daftar:
#include <concepts>
#include <iostream>
#include <string>
#include <utility>
#include <vector>
// Policy 1: bagaimana hasil dicetak
struct CetakRapi {
static void cetak(const std::string& pesan) {
std::cout << "[INFO] " << pesan << "\n";
}
};
// Policy diam: menerima panggilan, tidak mencetak apa pun
struct CetakDiam {
static void cetak(const std::string&) {}
};
// Policy 2: pemisah antar item
struct DenganKoma {
static constexpr const char* pemisah = ", ";
};
struct DenganGaris {
static constexpr const char* pemisah = " - ";
};
// Class template yang menyusun dua policy menjadi satu perilaku
template <typename PolicyCetak, typename PolicyPemisah = DenganKoma>
requires requires(const PolicyCetak& p, std::string s) {
{ p.cetak(s) } -> std::same_as<void>;
{ PolicyPemisah::pemisah } -> std::convertible_to<const char*>;
}
class Daftar {
public:
void tambah(std::string item) { item_.push_back(std::move(item)); }
void tampilkan() const {
std::string gabungan;
for (std::size_t i = 0; i < item_.size(); ++i) {
if (i > 0) gabungan += PolicyPemisah::pemisah;
gabungan += item_[i];
}
PolicyCetak::cetak(gabungan);
}
private:
std::vector<std::string> item_;
};
int main() {
Daftar<CetakRapi> belanja;
belanja.tambah("beras");
belanja.tambah("minyak");
belanja.tampilkan();
Daftar<CetakRapi, DenganGaris> tiket;
tiket.tambah("A1");
tiket.tambah("A2");
tiket.tampilkan();
Daftar<CetakDiam> senyap;
senyap.tambah("catatan rahasia");
senyap.tampilkan();
std::cout << "(CetakDiam menerima panggilan tanpa mencetak apa pun)\n";
return 0;
}Output yang Dihasilkan:
[INFO] beras, minyak
[INFO] A1 - A2
(CetakDiam menerima panggilan tanpa mencetak apa pun)Bedah baris demi baris:
- Baris 8-17: dua policy keluaran dengan kontrak yang sama — method
staticbernamacetak().CetakDiamsengaja kosong: pola seperti ini berguna untuk menonaktifkan pencatatan (logging) tanpa mengubah kode pemanggil. - Baris 20-26: policy pemisah tidak punya method, hanya konstanta
pemisah. Policy boleh berupa bentuk apa pun; yang penting host tahu apa yang boleh ditanyakan. - Baris 29-33: requires clause dengan pola
requires requiresdari Chapter requires Expressions menjadi kontrak tertulis bagi policy:cetak()harus mengembalikanvoiddanpemisahharus ada. Policy yang salah kini ditolak dengan pesan yang jelas, bukan error yang tak terjelaskan di tengah instansiasi. - Baris 41 dan 44: class host memanggil policy seperti fungsi biasa. Karena semuanya selesai saat kompilasi, compiler bisa men-inline seluruh panggilan — komposisi tanpa satu biaya runtime pun.
- Baris 29 (
PolicyPemisah = DenganKoma): parameter template kedua diberi nilai bawaan.Daftar<CetakRapi>pada baris 52 cukup satu argumen, sedangkantiketpada baris 57 menukar policy pemisahnya saja. - Baris 52-65: tiga kombinasi policy, tiga perilaku berbeda, satu class host. Menambah perilaku baru berarti menulis struct kecil — bukan mengubah
Daftar.
Tag Dispatch: Keputusan dari Label Tipe #
Kita pernah menghitung jarak antar iterator lewat std::distance di Section Iterators dan Algorithms. Di balik layar, fungsi itu menghadapi dilema: untuk std::vector pengurangan akhir - awal selesai dalam O(1), tetapi untuk std::list yang harus maju satu-satu butuh O(n). Solusi standarnya adalah tag dispatch: setiap tipe iterator membawa label berupa iterator category — sebuah class kosong seperti std::random_access_iterator_tag — dan fungsi publik meneruskannya sebagai argumen agar compiler memilih overload yang tepat.
#include <iostream>
#include <iterator>
#include <list>
#include <type_traits>
#include <vector>
// Fungsi yang menjawab: iterator ini milik kategori apa?
template <typename Iterator>
const char* kategoriIterator(Iterator) {
using Tag = typename std::iterator_traits<Iterator>::iterator_category;
if constexpr (std::is_base_of_v<std::random_access_iterator_tag, Tag>) {
return "random access - bisa melompat lewat indeks";
} else if constexpr (std::is_base_of_v<std::bidirectional_iterator_tag, Tag>) {
return "bidirectional - maju-mundur satu langkah";
} else if constexpr (std::is_base_of_v<std::forward_iterator_tag, Tag>) {
return "forward - maju saja";
} else {
return "input - sekali baca";
}
}
// Tag dispatch: dua versi jarak(), compiler memilih lewat "label tipe"
template <typename Iterator>
std::size_t jarak(Iterator awal, Iterator akhir, std::input_iterator_tag) {
std::size_t n = 0;
for (; awal != akhir; ++awal) {
++n; // maju satu-satu: O(n)
}
return n;
}
template <typename Iterator>
std::size_t jarak(Iterator awal, Iterator akhir, std::random_access_iterator_tag) {
return static_cast<std::size_t>(akhir - awal); // selisih langsung: O(1)
}
// Fungsi publik: menyerahkan keputusan kepada tag yang dibawa tipenya
template <typename Iterator>
std::size_t jarak(Iterator awal, Iterator akhir) {
using Tag = typename std::iterator_traits<Iterator>::iterator_category;
return jarak(awal, akhir, Tag{});
}
int main() {
std::vector<int> v{10, 20, 30, 40, 50};
std::list<int> l{10, 20, 30, 40, 50};
std::cout << "vector: " << kategoriIterator(v.begin()) << "\n";
std::cout << "list : " << kategoriIterator(l.begin()) << "\n";
std::cout << "jarak vector: " << jarak(v.begin(), v.end()) << "\n";
std::cout << "jarak list : " << jarak(l.begin(), l.end()) << "\n";
return 0;
}Output yang Dihasilkan:
vector: random access - bisa melompat lewat indeks
list : bidirectional - maju-mundur satu langkah
jarak vector: 5
jarak list : 5Bedah baris demi baris:
- Baris 10 (
iterator_traits<Iterator>::iterator_category): tag adalah tipe tanpa data — tugasnya hanya satu, menjadi identitas.std::iterator_traitsmengambil label bawaan setiap tipe iterator. - Baris 12-20: hierarki tag ditanya dengan
is_base_of_vdi dalamif constexprdari Chapter Compile-time Constraints. Tag bidirectional adalah turunan tag forward, dan tag forward turunan tag input — jadi urutan pengecekannya dari yang paling kuat. - Baris 24-31 dan 33-36: dua implementasi
jarakdengan parameter tag berbeda sebagai argumen ketiga. Yang dipilih compiler bukan lewat kondisi runtime, melainkan lewat resolusi overload biasa. - Baris 42 (
jarak(awal, akhir, Tag{})): fungsi publik membuat nilai tag dari tipe iterator, lalu compiler menyambungkannya ke overload yang cocok. Di sinilah keputusan “jalur mana” diambil — tetap saat kompilasi. - Baris 52-53: hasilnya keduanya
5, tetapi jalannya berbeda. Iterator vector membawa tag random access (C++20 menambah tagcontiguousyang masih turunannya), sedangkan iterator list membawa tag bidirectional yang cocok ke overload input lewat konversi turunan-ke-dasar.
Pola ini juga menjadi sumber pesan error paling terkenal bagi pemula STL: mengirim iterator list ke std::sort memicu dinding error tentang tag yang tidak memenuhi kendala. Setelah memahami tag dispatch, pesan semacam itu berubah dari teka-teki menjadi informasi.
CRTP: Mewarisi Tanpa Virtual #
Polymorphism pada Section Pemrograman Berorientasi Object menyelesaikan pemanggilan method saat program berjalan lewat virtual function — fleksibel, tetapi ada biaya indireksinya. Curiously Recurring Template Pattern (CRTP) membalik keadaan: class dasar menerima class turunannya sendiri sebagai parameter template, sehingga compiler tahu persis class apa yang sedang dipanggil dan menyelesaikan semuanya saat kompilasi.
#include <iostream>
#include <string>
// CRTP: class dasar menerima class turunannya sendiri sebagai parameter
template <typename Turunan>
class BisaCetak {
public:
void cetak() const {
// compiler tahu persis: Turunan di sini adalah class pemanggil
const Turunan& diri = static_cast<const Turunan&>(*this);
std::cout << "[" << diri.nama() << "] " << diri.detail() << "\n";
}
};
class Pesanan : public BisaCetak<Pesanan> {
public:
std::string nama() const { return "Pesanan"; }
std::string detail() const { return "total 3 item, siap kirim"; }
};
class Inventaris : public BisaCetak<Inventaris> {
public:
std::string nama() const { return "Inventaris"; }
std::string detail() const { return "4 gudang aktif"; }
};
int main() {
Pesanan p;
Inventaris i;
p.cetak();
i.cetak();
return 0;
}Output yang Dihasilkan:
[Pesanan] total 3 item, siap kirim
[Inventaris] 4 gudang aktifBedah baris demi baris:
- Baris 5-6 (
template <typename Turunan> class BisaCetak): class dasar tidak mengenal dirinya sendiri sebagai tipe tetap — ia menunggu diisi nama class turunannya. Bentukclass Pesanan : public BisaCetak<Pesanan>itulah yang membuat polanya terlihat melingkar dan “curiously recurring”. - Baris 10:
static_cast<const Turunan&>(*this)memandangthissebagai class turunan. Aman karena satu-satunya cara class dasar ini dipakai adalah lewar pewarisan seperti baris 15 dan 21 — kontraknya jelas meski tidak diperiksa compiler. - Baris 11: pemanggilan
diri.nama()dandiri.detail()bukan virtual call. UntukPesanan, compiler langsung menautkan kePesanan::nama()— perilaku yang sama dengan override, tanpa vtable. - Baris 15-24: dua class hanya perlu menyediakan method yang dituntut.
BisaCetakbertindak sebagai mixin: sekumpulan kemampuan yang ditambahkan ke class mana pun yang menyediakan bahan bakunya. - Baris 30-31: kedua pemanggilan menghasilkan keluaran berbeda dari method yang ditulis sekali di class dasar. Satu definisi
cetak(), dua perilaku — tergantung tipe instansiasinya.
Polinya tersebar luas di pustaka standar dan pustaka pihak ketiga, misalnya std::enable_shared_from_this yang kita temui saat membahas shared_ptr. Aturan praktisnya: bila polymorphism dibutuhkan dan tipe sudah diketahui saat kompilasi, CRTP memberi hasil sama dengan biaya lebih rendah.
Type Erasure: Antarmuka yang Menyembunyikan Tipe #
Ketiga pola di atas menuntut tipe diketahui saat kompilasi. Lalu bagaimana dengan std::function yang bisa menampung lambda, pointer function, sekaligus function object — tiga tipe berbeda dalam satu wadah? Jawabannya type erasure: bungkus tipe apa pun di balik antarmuka kecil, lalu buang nama tipenya dari tanda tangan yang dilihat pengguna. std::function<void()> dan std::any yang kita kenal di chapter Function Objects dan std::any bekerja dengan resep ini — di dalamnya ada lapisan template yang mengadaptasi tipe asli ke antarmuka bersama, sering kali dikombinasikan dengan virtual function di bagian internalnya.
Type erasure adalah titik temu generik dan OOP: luarannya terasa seperti OOP (satu antarmuka, banyak tipe, diputuskan saat runtime), sedangkan mesin di baliknya memanfaatkan template agar class luar tidak perlu diubah untuk setiap tipe baru. Pustaka besar seperti menulis antarmuka gambar yang menerima apa pun yang “bisa digambar” memakai pola ini ketika inheritance hierarchy terasa memaksa. Untuk kursus ini, mengenali polanya cukup — implementasi lengkapnya layak ditunda sampai kebutuhan nyata muncul.
Kapan Generik, Kapan OOP #
Kedua paradigma kini sama-sama kita kuasai, sehingga pilihannya bisa dibuat dengan sadar. Ringkasannya:
| Pertanyaan | Generik (template) | OOP (virtual function) |
|---|---|---|
| Kapan tipe diketahui? | saat kompilasi | boleh baru saat runtime |
| Tipe campuran dalam satu wadah? | sulit — perlu type erasure | mudah lewat pointer atau reference dasar |
| Biaya per panggilan | bisa di-inline, tanpa indireksi | lewat vtable, biasanya tidak di-inline |
| Ukuran binary | duplikasi kode per tipe | satu salinan kode bersama |
| Antarmuka | “punya operasi yang dibutuhkan” (duck typing saat kompilasi) | “mewarisi class dasar” |
Panduan praktisnya begini. Pilih generik ketika kumpulan tipe sudah tetap saat kompilasi dan kode yang sama harus jalan untuk semuanya — container, algorithm, pustaka utilitas; di sanalah STL berdiri. Pilih OOP ketika program benar-benar menangani object yang jenisnya baru ditentukan pengguna saat runtime — plugin, node dalam simulasi, item dalam editor. Keduanya bisa hidup berdampingan: pustaka besar sering memakai template di dalam dan antarmuka virtual di luar, dengan type erasure sebagai jembatannya.
Penutup Section: Peta Lengkap Generic Programming #
Section Generic Programming dan Templates resmi selesai. Perjalanan empat belas chapternya bisa dirangkum dalam satu tabel:
| Chapter | Warisannya |
|---|---|
| Function Templates | satu resep untuk semua tipe |
| Class Templates | container sendiri, dari Kotak<T> |
| Template Parameters | anatomi parameter: tipe, nilai bawaan, deduksi |
| Non-type Template Parameters | nilai — bukan hanya tipe — sebagai parameter |
| Template Specialization | versi khusus untuk tipe tertentu |
| Partial Specialization | khusus pada sebagian pola, plus overload |
| Variadic Templates | jumlah parameter yang tak terbatas |
| Parameter Pack | expansion pattern dan pemisahan pack |
| Fold Expressions | melipat operator ke seluruh pack |
| Type Traits | bertanya tentang tipe pada saat kompilasi |
| Concepts | kendala bernama dengan pesan error yang menjelaskan diri |
| requires Expressions | menguji kemampuan tipe secara ad-hoc |
| Compile-time Constraints | static_assert, kendala class, dan if constexpr |
| Generic Programming Patterns | policy, tag dispatch, CRTP, type erasure |
Dari sini ada dua pintu lanjutan yang saling melengkapi. Pertama, Section Modern C++ menelusuri fitur-fitur yang selama ini kita pakai tanpa basa-basi — if constexpr dari C++17, concept dari C++20, hingga berita terbaru C++23 dan C++26 — dimulai dari Chapter Fitur C++11. Kedua, Section Move Semantics dan Value Categories membuka babak lanjutan tentang cara object dipindahkan dan disalin, fondasi untuk template yang efisien. Sampai jumpa di sana.
Referensi resmi: cppreference — Iterator tags , cppreference — CRTP , dan cppreference — std::function .