Idiom dan design pattern adalah pengetahuan kolektif komunitas C++ berupa cara yang teruji untuk memecahkan masalah berulang tanpa merancang solusi baru setiap saat. Idiom umumnya berukuran kecil dan menyentuh teknik bahasa tertentu, sedangkan design pattern mengatur struktur antarkelas pada tingkat yang lebih tinggi. Modul ini membahas tiga idiom yang paling sering muncul di kode C++ profesional, yaitu pimpl, non-virtual interface, dan CRTP, ditutup dengan pertimbangan kapan masing-masing layak dipakai.
Pimpl: menyembunyikan detail implementasi#
Idiom pimpl (pointer to implementation) menyembunyikan seluruh member private kelas di balik pointer ke struktur internal. Header kelas hanya memuat deklarasi maju struct Impl beserta std::unique_ptr<Impl>, sehingga perubahan pada implementasi tidak lagi memicu kompilasi ulang berkas lain yang memasukkan header tersebut. Teknik ini memutus rantai ketergantungan kompilasi yang sering menghambat proyek besar.
Contoh berikut dipisah menjadi tiga berkas. Header pengolah.h hanya menyatakan antarmuka, pengolah.cpp menampung seluruh detail Impl, dan utama.cpp memakai kelas tanpa pernah tahu isi implementasinya.
// tidak-dijalankan (build multi-berkas; perintah ada di bawah)
#ifndef PENGOLAH_H
#define PENGOLAH_H
#include <memory>
#include <string>
class Pengolah {
public:
Pengolah();
~Pengolah();
Pengolah(const Pengolah&) = delete;
Pengolah& operator=(const Pengolah&) = delete;
void tambah(double nilai);
double rataRata() const;
std::string ringkasan() const;
private:
struct Impl;
std::unique_ptr<Impl> impl_;
};
#endif// tidak-dijalankan (build multi-berkas; perintah ada di bawah)
#include "pengolah.h"
#include <sstream>
// seluruh detail implementasi hidup di berkas ini saja
struct Pengolah::Impl {
double total = 0.0;
int banyak = 0;
};
Pengolah::Pengolah() : impl_(std::make_unique<Impl>()) {}
Pengolah::~Pengolah() = default;
void Pengolah::tambah(double nilai) {
impl_->total += nilai;
++impl_->banyak;
}
double Pengolah::rataRata() const {
return impl_->banyak == 0 ? 0.0 : impl_->total / impl_->banyak;
}
std::string Pengolah::ringkasan() const {
std::ostringstream keluar;
keluar << "banyak=" << impl_->banyak << ", rata-rata=" << rataRata();
return keluar.str();
}// tidak-dijalankan (build multi-berkas; perintah ada di bawah)
#include <iostream>
#include "pengolah.h"
int main() {
Pengolah pengolah;
pengolah.tambah(80.0);
pengolah.tambah(90.0);
pengolah.tambah(85.0);
std::cout << pengolah.ringkasan() << '\n';
return 0;
}g++ -std=c++17 utama.cpp pengolah.cpp -o utama
./utamabanyak=3, rata-rata=85Perhatikan dua konsekuensi dari struktur di atas. Pertama, destructor harus dinyatakan di header dan didefinisikan di pengolah.cpp, karena std::unique_ptr<Impl> membutuhkan ukuran Impl yang lengkap pada saat destructor dibuat. Kedua, operasi penyalinan sengaja dihapus karena membiarkan salinan dangkal akan membuat dua objek berbagi Impl yang sama dan menimbulkan error double-free. Sebagai imbalannya, batas ABI kelas menjadi stabil. Menambah member baru ke Impl tidak mengubah tata letak memori Pengolah, sehingga pustaka yang sudah dikompilasi tidak perlu dibangun ulang oleh penggunanya.
Biaya utama pimpl adalah satu alokasi heap tambahan untuk setiap objek yang dibuat. Aplikasi yang membuat jutaan objek kecil akan merasakan penurunan performa dan fragmentasi memori, sehingga beberapa pustaka menerapkan varian fast pimpl yang menyediakan penyimpanan berukuran tetap di dalam objek itu sendiri dan hanya beralih ke alokasi heap bila kapasitas terlampaui. Varian semacam ini menuntut disiplin ekstra karena ukuran maksimum Impl harus dijamin selamanya. Teknik ini karenanya baru dipertimbangkan setelah pengukuran menunjukkan alokasi memang menjadi hambatan.
Non-virtual interface: kerangka tetap, langkah bisa diganti#
Idiom non-virtual interface (NVI) membalik tata letak kelas dasar pada umumnya. Fungsi virtual dibuat privat, sedangkan fungsi publik yang non-virtual menjadi pembungkus kerangka alur. Kelas turunan hanya boleh mengganti langkah-langkah kecil yang dipanggil oleh kerangka tersebut, bukan kerangkanya sendiri. Pola ini juga dikenal sebagai Template Method di literatur design pattern.
#include <iostream>
// kerangka alur tetap di kelas dasar dan tidak dapat diganti
class AlurData {
public:
void proses() const {
std::cout << "mulai\n";
ambilData();
std::cout << "selesai\n";
}
virtual ~AlurData() = default;
private:
// langkah yang boleh diganti oleh kelas turunan
virtual void ambilData() const = 0;
};
class SensorSuhu : public AlurData {
private:
void ambilData() const override {
std::cout << "ambil: suhu\n";
}
};
class SensorTekanan : public AlurData {
private:
void ambilData() const override {
std::cout << "ambil: tekanan\n";
}
};
int main() {
SensorSuhu suhu;
SensorTekanan tekanan;
suhu.proses();
tekanan.proses();
return 0;
}mulai
ambil: suhu
selesai
mulai
ambil: tekanan
selesaiKekuatan NVI terletak pada satu titik kendali. Kelas dasar bebas menambahkan validasi, pencatatan, pengukuran waktu, atau penguncian mutex di sekitar ambilData() tanpa menyentuh satu pun kelas turunan, karena semua panggilan melewati proses(). Invarian seperti “data harus dibuka sebelum diproses” tidak mungkin dilanggar oleh kelas turunan, berbeda dengan desain yang mengekspos fungsi virtual secara langsung. Perhatikan pula bahwa destructor tetap dibuat publik dan virtual, karena pelepasan objek melalui pointer ke kelas dasar pada polimorfisme runtime hanya benar apabila destructor bersifat virtual.
NVI juga membawa biaya yang perlu diketahui sebelum diterapkan. Setiap langkah yang bisa diganti kini membutuhkan dua fungsi, yaitu pembungkus publik dan fungsi virtual privat, sehingga jumlah member kelas bertambah dan header memuat lebih banyak deklarasi. Untuk fungsi sepele yang dipanggil sangat sering, satu lapisan panggilan tambahan biasanya masih dihapus oleh compiler, tetapi kerumitan deklarasi tetap ada. Karena itu NVI paling bermanfaat pada kelas dasar dengan alur yang benar-benar memiliki invarian, dan berlebihan pada kelas yang fungsi virtualnya berdiri sendiri tanpa urutan yang harus dijaga.
CRTP: polimorfisme tanpa biaya virtual#
Curiously Recurring Template Pattern (CRTP) membalik arah pewarisan biasa. Kelas dasar adalah template yang menerima nama kelas turunannya sendiri sebagai parameter. Dengan static_cast ke Turunan, kelas dasar dapat memanggil member kelas turunan tanpa fungsi virtual sama sekali. Pemilihan fungsi pun terjadi saat kompilasi, sehingga compiler dapat menyisipkan (inline) panggilan dan tidak ada biaya pointer vtable.
#include <iostream>
#include <string>
// Laporan menerima kelas turunannya sendiri sebagai parameter template
template <typename Turunan>
class Laporan {
public:
void cetak() const {
const Turunan& diri = static_cast<const Turunan&>(*this);
std::cout << diri.judul() << ": " << diri.isi() << '\n';
}
};
class RekapNilai : public Laporan<RekapNilai> {
public:
std::string judul() const { return "Rekap nilai"; }
int isi() const { return 78; }
};
class JumlahBaris : public Laporan<JumlahBaris> {
public:
std::string judul() const { return "Jumlah baris"; }
int isi() const { return 1523; }
};
int main() {
RekapNilai rekap;
JumlahBaris baris;
rekap.cetak();
baris.cetak();
return 0;
}Rekap nilai: 78
Jumlah baris: 1523Pola ini sering dipakai untuk mixin statis, misalnya kelas BisaDihitung yang menambahkan operator perbandingan ke setiap kelas yang mewarisinya melalui static_cast<const Turunan&>(*this). Kelemahannya, kelas RekapNilai dan JumlahBaris tidak berbagi tipe dasar yang sama, sehingga keduanya tidak dapat disimpan dalam satu std::vector dengan tipe elemen yang seragam. Karena itu CRTP menggantikan polimorfisme runtime, bukan menirunya. CRTP dipilih ketika kecepatan dan ketiadaan indireksi lebih penting daripada kebutuhan mencampur berbagai tipe dalam satu wadah.
Kapan idiom ini layak dipakai#
Setiap idiom di atas mengorbankan sesuatu untuk mendapat keuntungan tertentu, sehingga keputusannya sebaiknya didasarkan pada masalah yang benar-benar terjadi. Beberapa panduan praktis:
- Pimpl dipakai ketika header besar terus memicu kompilasi ulang berantai, ketika stabilitas ABI pustaka menjadi syarat kontrak, atau ketika detail implementasi perlu dirahasiakan dari pengguna pustaka.
- NVI dipakai ketika kelas dasar mendefinisikan alur yang urutannya tidak boleh dilanggar, atau ketika validasi dan pencatatan perlu melekat pada satu titik panggilan saja.
- CRTP dipakai ketika polimorfisme hanya dibutuhkan saat kompilasi, misalnya untuk mixin kemampuan atau penghapusan duplikasi kode antar kelas yang serupa.
Ringkasan#
- Pimpl menyembunyikan data anggota di balik
std::unique_ptr<Impl>, memutus rantai kompilasi ulang, dan menjaga ABI tetap stabil dengan biaya satu alokasi per objek. - NVI membuat fungsi virtual menjadi privat dan menaruh kerangka alur pada fungsi publik non-virtual sehingga invarian kelas dasar tidak dapat dilanggar.
- CRTP menyediakan polimorfisme saat kompilasi tanpa vtable, cocok untuk mixin statis, tetapi tidak menghasilkan tipe dasar yang seragam.
- Ketiganya menuntut biaya tertentu, jadi pilihlah berdasarkan masalah nyata, bukan sekadar keinginan kode terlihat canggih.
Referensi#
- pimpl — cppreference.com — Penjelasan idiom pimpl beserta variannya dan pertimbangan biayanya.
- CRTP — cppreference.com — Definisi Curiously Recurring Template Pattern dan contoh pemakaiannya.
- When should someone use private virtuals? — ISO C++ FAQ — Alasan memakai fungsi virtual privat sebagai fondasi idiom non-virtual interface.