Kelas abstrak dan pure virtual

Antarmuka dalam C++ dan perancangan kontrak bagi kelas turunan.

Daftar isi

Topik ini membahas pure virtual function dan kelas abstrak, yaitu pasangan mekanisme yang menjadikan kelas dasar sebagai antarmuka murni tanpa implementasi yang wajib. Materinya mencakup sintaks penanda = 0, akibatnya terhadap kelas yang memilikinya, perancangan kontrak bagi kelas turunan, serta polimorfisme yang mengoperasikan sekumpulan bentuk berbeda melalui satu tipe antarmuka. Setelah membacanya, pembaca dapat merancang kelas antarmuka yang menuntut setiap kelas turunan memenuhi perjanjiannya.

Pure virtual function dan kelas abstrak#

Virtual function pada topik sebelumnya tetap memiliki implementasi di kelas dasar yang dapat dipakai ulang kelas turunannya. Sebaliknya, sebuah member function dapat dinyatakan tanpa implementasi sama sekali dengan menambahkan = 0 pada deklarasinya, dan bentuk ini disebut pure virtual function. Kelas yang memiliki satu atau lebih pure virtual function disebut kelas abstrak, dan compiler melarang pembuatan objek langsung dari kelas tersebut. Kelas abstrak hanya digunakan sebagai kelas perantara, yaitu kelas yang diwariskan dan diisi implementasinya oleh kelas turunan. Antarmuka bentuk geometris berikut memuat dua pure virtual function beserta satu member function yang sudah lengkap.

cpp bentuk-abstrak.cpp
#include <iomanip>
#include <iostream>
#include <string>
#include <vector>

class Bentuk {
public:
    virtual double luas() const = 0;

    virtual std::string nama() const = 0;

    void ringkas() const {
        std::cout << nama() << " memiliki luas "
                  << std::fixed << std::setprecision(2) << luas() << "\n";
    }
};

class PersegiPanjang : public Bentuk {
public:
    PersegiPanjang(double p, double l) : panjang{p}, lebar{l} {}

    double luas() const override {
        return panjang * lebar;
    }

    std::string nama() const override {
        return "Persegi panjang";
    }

private:
    double panjang;
    double lebar;
};

class Lingkaran : public Bentuk {
public:
    explicit Lingkaran(double r) : radius{r} {}

    double luas() const override {
        return 3.14159265358979 * radius * radius;
    }

    std::string nama() const override {
        return "Lingkaran";
    }

private:
    double radius;
};

int main() {
    PersegiPanjang p{4.0, 3.0};
    Lingkaran l{2.0};

    std::vector<Bentuk*> daftar{&p, &l};
    for (const Bentuk* b : daftar) {
        b->ringkas();
    }
    return 0;
}
text Keluaran
Persegi panjang memiliki luas 12.00
Lingkaran memiliki luas 12.57

Perhitungan luas dijalankan compiler melalui dynamic dispatch, yaitu luas persegi panjang p×lp \times l diambil dari PersegiPanjang::luas, sedangkan luas lingkaran πr2\pi r^2 diambil dari Lingkaran::luas. Member function ringkas tidak virtual dan tidak pernah didefinisikan ulang, tetapi member function itu bekerja untuk semua bentuk karena hanya memanggil fungsi antarmuka yang dijamin tersedia. Pola seperti ini menempatkan logika umum di kelas dasar tanpa perlu mengetahui detail perhitungan tiap kelas turunannya. Pointer pada program ini hanya mengamati objek yang hidup di main dan tidak melakukan penghapusan apa pun, sedangkan aturan destructor untuk kepemilikan dinamis dibahas pada topik tersendiri dalam modul ini.

Baris berikut memperlihatkan larangan yang menyertai kelas abstrak dan kelas turunan yang belum menuntaskan kontraknya.

cpp Contoh: kelas abstrak tidak dapat dijadikan objek
// tidak-dijalankan (Bentuk masih memiliki pure virtual function, sehingga tidak dapat dijadikan objek)
Bentuk b;
cpp Contoh: kelas turunan yang belum menuntaskan kontrak tetap abstrak
// tidak-dijalankan (luas belum diimplementasikan, sehingga Segitiga tetap kelas abstrak)
class Segitiga : public Bentuk {
public:
    std::string nama() const override {
        return "Segitiga";
    }
};

Segitiga s;

Pada fragmen kedua, Segitiga yang hanya mengisi nama masih dianggap kelas abstrak karena kewajiban luas belum terpenuhi. Baris pembuatan objeknya ditolak compiler dengan pesan yang menyatakan kelas itu masih abstrak, dan penolakan ini justru berguna, yaitu mencegah objek setengah jadi ikut berputar di dalam program.

Antarmuka sebagai kontrak#

Kelas abstrak yang hanya memuat pure virtual function tanpa member data berperan sebagai antarmuka, yaitu daftar kemampuan yang dijanjikan setiap kelas turunannya. Kontrak pada kelas Bentuk berbunyi sederhana, yaitu setiap bentuk wajib melaporkan luasnya dan menamai dirinya, sedangkan cara menghitungnya sepenuhnya menjadi urusan kelas turunan masing-masing. Kode pengguna kemudian ditulis terhadap kontrak itu, bukan terhadap tipe konkret, sehingga menambah jenis bentuk baru tidak mengubah satu baris pun di dalam perulangan main. Batasan invarian dan prinsip enkapsulasi dari modul kelas dan objek tetap berlaku, sedangkan penanda override dari topik virtual function memastikan setiap definisi ulang memakai tanda tangan fungsi yang benar.

Pure virtual function juga dapat diberi implementasi bawaan yang tetap dipanggil secara eksplisit oleh kelas turunan. Namun, kebutuhan semacam itu lebih tepat ditulis sebagai virtual function biasa dengan isi, sehingga seluruh kelas turunan mewarisi perilakunya tanpa langkah tambahan. Kelas antarmuka sebaiknya hanya memuat operasi yang memang berbeda antar kelas turunan, sedangkan perilaku yang sama untuk semua turunan dipindahkan ke member function non-virtual sebagaimana ringkas. Antarmuka yang ramping lebih mudah dipenuhi, sedangkan antarmuka yang menumpuk kewajiban memaksa kelas turunan mengisi banyak hal yang tidak relevan bagi kelas tersebut.

Menulis = 0 berbeda maknanya dengan menulis badan fungsi kosong. Virtual function dengan badan kosong tetap menyediakan implementasi, yaitu perilaku tidak melakukan apa pun, sehingga kelasnya masih sah dijadikan objek dan kontraknya tidak dipaksakan. Penanda = 0 justru menyatakan tidak adanya perilaku default yang masuk akal, sehingga keputusan perhitungan luas wajib ditentukan oleh kelas turunan yang memang mengetahui bentuknya. Pembedaan inilah yang menjadikan pure virtual function alat yang tepat untuk menuntut jawaban, bukan sekadar memberi jawaban kosong.

Kesalahan umum#

Empat kesalahan berikut paling sering muncul saat kelas abstrak dirancang dan dipakai.

  • Lupa mengisi salah satu pure virtual function di kelas turunan. Kelas itu menjadi abstrak tanpa peringatan, dan error baru muncul pada baris pembuatan objeknya. Perbaikannya adalah membaca pesan compiler yang menyebut fungsi mana yang belum diimplementasikan, lalu menandai setiap definisi ulang dengan override agar kekurangannya tertangkap lebih awal.
  • Mencoba membuat objek dari kelas antarmuka. Kelas seperti Bentuk hanya menjadi tipe reference, pointer, dan parameter, sedangkan objeknya selalu dibuat dari kelas turunan yang implementasinya lengkap.
  • Menaruh member data ke dalam kelas antarmuka. Data milik kelas dasar ikut tersusun pada setiap turunannya dan mengikat semuanya pada satu bentuk penyimpanan, sehingga antarmuka kehilangan sifatnya sebagai daftar kemampuan murni. Perbaikannya adalah menaruh data pada kelas turunan yang memang membutuhkannya.
  • Menyimpan pointer ke objek yang masa hidupnya sudah berakhir, misalnya alamat objek lokal yang dibuat di dalam badan perulangan. Pointer antarmuka hanya sah selama objek yang diamatinya masih hidup, sehingga objeknya perlu dibuat pada lingkup yang lebih panjang daripada penggunaan pointernya.

Ringkasan#

  • Penanda = 0 menjadikan member function pure virtual, dan kelas yang memilikinya menjadi kelas abstrak yang tidak dapat dijadikan objek.
  • Kelas turunan wajib mengisi seluruh pure virtual function milik kelas dasarnya, dan kelas yang belum tugas tetap abstrak.
  • Kelas antarmuka menyatakan kontrak berupa daftar operasi, sedangkan logika yang sama untuk semua turunan ditulis sebagai member function non-virtual di kelas dasarnya.
  • Polimorfisme lewat pointer atau reference antarmuka mengoperasikan campuran kelas turunan tanpa mengetahui tipe konkret masing-masing.

Referensi#