Chapter sebelumnya memperkenalkan pure virtual function sebagai penanda class abstract. Kali ini kita kupas mekanismenya sampai ke akar: apa yang sebenarnya dilakukan = 0, mengapa turunan wajib mengisi atau ikut menjadi abstract, dan satu kejutan yang jarang diketahui pemula — pure virtual function boleh punya implementasi default yang dipanggil turunan lewat Dasar::nama(). Kombinasi ini menjadikan pure virtual lebih lentur daripada reputasinya: ia memaksa kontrak diisi, tetapi tetap bisa menyediakan jawaban standar untuk yang membutuhkannya.
Sintaks = 0 dan Kewajiban Turunan #
Menambahkan = 0 pada deklarasi virtual function mengubahnya menjadi pure virtual:
virtual void cetakHeader() const = 0; // murni: tidak ada isi di deklarasi ini
Konsekuensinya berlaku dua arah. Ke arah atas: class pemiliknya langsung menjadi abstract, tidak bisa diinstansiasi. Ke arah bawah: setiap class turunan wajib meng-override-nya — bila tidak, turunan itu mewarisi status abstract dan sama-sama tidak bisa dibuat. Berbeda dengan virtual biasa yang menawarkan versi default dan diam bila tidak di-override, pure virtual menutup jalan tengah: isi kontrak, atau jadilah cetakan kosong.
Karena itulah pure virtual adalah alat paling tegas untuk menyatakan “turunan dari class ini pasti bisa melakukan ini”. Compiler memeriksa kewajiban itu pada setiap class yang mencoba menjadi konkret, sehingga tidak ada satu pun celah lupa — berbeda dari virtual biasa yang jawaban default-nya bisa masuk akal untuk sebagian kasus, tetapi berbahaya bila diam-diam dipakai object yang seharusnya punya perilaku sendiri.
Pure Virtual dengan Implementasi Default #
Ini bagian yang paling sering mengejutkan: = 0 tidak melarang adanya implementasi. Sintaksnya terlihat aneh pada awalnya — dideklarasikan murni di dalam class, didefinisikan di luar class:
virtual void cetakIsi() const = 0; // di dalam class: murni
...
void Laporan::cetakIsi() const { // di luar class: implementasinya
std::cout << "Isi laporan standar perusahaan\n";
}Class tetap abstract — kewajiban override tidak bergeser sedikit pun — tetapi kini ada jawaban standar yang tersimpan di class dasar. Turunan yang meng-override boleh meminjamnya dengan panggilan eksplisit Laporan::cetakIsi() di dalam badan override-nya sendiri, persis pola yang kita kenal dari Chapter Overriding. Perhatikan: tanpa override, default ini tidak pernah bisa terpanggil — object abstract tidak bisa dibuat, dan dynamic dispatch tidak punya jalur ke sana; satu-satunya pintunya adalah nama lengkap Laporan::cetakIsi() dari dalam kode turunan.
Untuk apa pola ini dipakai? Ketika sebagian besar turunan membutuhkan langkah yang sama, tetapi langkah itu sendiri tidak bermakna sebagai “perilaku dasar yang bisa berdiri sendiri”. Klasiknya di dunia nyata: destructor virtual murni yang tetap butuh badan, atau metode pembayaran yang “tahapan validasinya sama, hanya instrumennya beda” — sisanya tinggal ditambahkan turunan.
Contoh Lengkap: Kontrak dengan Jawaban Standar #
Program berikut memakai kedua bentuk pure virtual: cetakHeader() benar-benar tanpa implementasi, dan cetakIsi() murni namun berbekal implementasi default di luar class:
#include <iostream>
class Laporan {
public:
virtual void cetakHeader() const = 0; // murni: tanpa implementasi sama sekali
virtual void cetakIsi() const = 0; // murni JUGA, tapi punya implementasi default
// Method konkret yang mengatur alur
void cetak() const {
cetakHeader();
cetakIsi();
}
};
// Implementasi default untuk pure virtual: ditulis di LUAR class
void Laporan::cetakIsi() const {
std::cout << "Isi laporan standar perusahaan\n";
}
class LaporanPenjualan : public Laporan {
public:
void cetakHeader() const override {
std::cout << "=== LAPORAN PENJUALAN ===\n";
}
void cetakIsi() const override {
Laporan::cetakIsi(); // pinjam implementasi default lewat Dasar::nama
std::cout << "Ditandatangani: Divisi Penjualan\n";
}
};
class LaporanKeuangan : public Laporan {
public:
void cetakHeader() const override {
std::cout << "=== LAPORAN KEUANGAN ===\n";
}
void cetakIsi() const override {
std::cout << "Ringkasan neraca: seimbang\n"; // isi sendiri, tanpa default
}
};
int main() {
LaporanPenjualan penjualan;
LaporanKeuangan keuangan;
penjualan.cetak();
std::cout << "---\n";
keuangan.cetak();
return 0;
}Output yang Dihasilkan:
=== LAPORAN PENJUALAN ===
Isi laporan standar perusahaan
Ditandatangani: Divisi Penjualan
---
=== LAPORAN KEUANGAN ===
Ringkasan neraca: seimbangMari kita bedah:
- Baris 5 dan 7 (dua
= 0): keduanya menjadikanLaporanabstract. Perbedaan nasibnya ada di baris 20-22:cetakHeader()tidak punya implementasi di mana pun, sedangkancetakIsi()mendapat satu di luar class. - Baris 13-16 (
cetak()): method konkret memanggil dua kontrak. Ia tidak peduli jawabannya dari siapa — dynamic dispatch yang mengerjakannya saat object sudah konkret. - Baris 20-22 (definisi di luar class):
void Laporan::cetakIsi() constmemberi isi bagi function yang dideklarasikan murni. StatusLaporantetap abstract — definisi ini bukan “jalan pintas” melainkan cadangan yang hanya bisa dipinjam turunan. - Baris 32-35 (pinjam lewat
Laporan::cetakIsi()):LaporanPenjualanmeng-override lalu memanggil versi dasar dengan nama lengkapnya. Dua baris pertama output membuktikannya: teks standar tercetak, diikuti tambahan khas divisi. - Baris 41-44 (isi sendiri):
LaporanKeuanganmemilih tidak meminjam apa pun — kontraknya tetap terpenuhi, isi bebas. Keduanya sah; pure virtual hanya menuntut ada override, tidak mengatur isinya.
Turunan yang Lupa: Tetap Abstract #
Bila sebuah turunan mengisi cetakHeader() tetapi melupakan cetakIsi(), ia tidak menjadi konkret — ia mewarisi sisa kontrak yang belum dilunasi. Compiler menolak pembuatan object-nya dan menyebut persis kontrak mana yang belum diisi:
error: cannot declare variable 'g' to be of abstract type 'LaporanGaji'
note: because the following virtual functions are pure within 'LaporanGaji':
'virtual void Laporan::cetakIsi() const'Pesan seperti ini muncul jauh sebelum program dijalankan — inilah jaminan yang tidak bisa diberikan virtual biasa. Bila desainmu menuntut “pemanggil tidak boleh pernah menerima object yang lupa mengisi”, pure virtual adalah satu-satunya jawaban di C++; virtual dengan default memang lebih santai, tetapi santai itulah celahnya.
Dengan kontrak yang ditegakkan penuh ini, satu kebiasaan baik terasa kurang: object polymorphic kita masih bermain di stack. Chapter berikutnya menghadapi pertanyaan yang paling sering keliru di seluruh OOP C++ — apa yang terjadi ketika object di heap dihapus lewat pointer class dasar, dan mengapa destructor harus ikut virtual.
Rujukan resmi: Abstract classes (cppreference) , Virtual specifier (cppreference) .