Topik ini membahas pewarisan ganda (multiple inheritance), yaitu kemampuan sebuah kelas mewarisi lebih dari satu kelas dasar sekaligus, beserta masalah diamond yang menyertainya. Materinya mencakup sintaks dasar, kondisi yang menimbulkan dua salinan kelas dasar, perbaikannya dengan kelas dasar virtual, serta penilaian jujur mengenai kapan mekanisme ini layak dipakai. Setelah membacanya, hierarki dengan dua jalur pewarisan dapat ditulis dengan benar dan batasan mekanismenya sudah diketahui sebelum dipilih.
Sintaks pewarisan ganda#
Pewarisan ganda ditulis dengan mendaftar lebih dari satu kelas dasar setelah tanda titik dua, dipisahkan tanda koma, dan setiap kelas dasar membawa access specifier-nya sendiri. Kelas turunan kemudian memperoleh seluruh member dari semua kelas dasarnya, sehingga kemampuan yang berbeda-beda dapat digabungkan tanpa menulis ulang satu pun member. Program berikut memperagakan kelas yang mewarisi dua kemampuan sekaligus.
#include <iostream>
class Terbang {
public:
void terbang() const {
std::cout << "Terbang setinggi 10 meter\n";
}
};
class Renang {
public:
void renang() const {
std::cout << "Renang sejauh 5 meter\n";
}
};
class Bebek : public Terbang, public Renang {
public:
void bersuara() const {
std::cout << "Kwek kwek\n";
}
};
int main() {
Bebek bebek;
bebek.terbang();
bebek.renang();
bebek.bersuara();
return 0;
}Terbang setinggi 10 meter
Renang sejauh 5 meter
Kwek kwekKelas Bebek tidak menulis ulang terbang() maupun renang(), karena kedua member itu datang dari dua kelas dasar yang berbeda. Urutan penulisan kelas dasar juga berpengaruh, yaitu menentukan urutan konstruksi subobjek, sehingga bagian Terbang dibangun sebelum bagian Renang. Sampai di sini pewarisan ganda hanyalah perluasan sederhana dari pewarisan tunggal, dan kerumitan baru muncul ketika kedua jalur pewarisan bertemu lagi di kelas yang sama. Satu hal kecil perlu dijaga: apabila access specifier tidak ditulis, pewarisan pada kelas berbasis class menjadi private secara default. Karena itu, menulis public secara eksplisit pada setiap kelas dasar menjadi kebiasaan yang aman.
Masalah diamond#
Masalah diamond muncul ketika dua kelas dasar mewarisi kelas yang sama, sehingga kelas paling bawah memuat dua salinan dari kelas paling atas. Namanya diambil dari bentuk diagram pewarisannya yang menyerupai belah ketupat, yaitu kelas puncak di atas, dua kelas tengah di sisi kiri dan kanan, dan kelas gabungan di bawah. Program berikut memperagakan kondisi tersebut beserta akibatnya pada data member.
#include <iostream>
#include <string>
class Orang {
public:
std::string nama = "(tanpa nama)";
};
class Mahasiswa : public Orang {};
class Asisten : public Orang {};
class AsistenPraktikum : public Mahasiswa, public Asisten {};
int main() {
AsistenPraktikum ap;
ap.Mahasiswa::nama = "Dewi"; // menulis salinan jalur Mahasiswa
ap.Asisten::nama = "Dewi Asisten"; // menulis salinan jalur Asisten
std::cout << "via Mahasiswa: " << ap.Mahasiswa::nama << "\n";
std::cout << "via Asisten : " << ap.Asisten::nama << "\n";
return 0;
}via Mahasiswa: Dewi
via Asisten : Dewi AsistenKeluaran tersebut memperlihatkan dua subobjek Orang yang saling terpisah, karena penulisan melalui jalur Mahasiswa tidak berpengaruh pada salinan milik jalur Asisten. Kedua salinan itu terbentuk tanpa peringatan apa pun saat kompilasi, sehingga keberadaannya baru diketahui ketika akses ambigu ditolak compiler. Akses tanpa kualifikasi jalur bahkan tidak dapat dikompilasi, sebagaimana dicatat berikut ini.
Ambiguitas serupa juga menimpa fungsi member, bukan hanya data member, sehingga kualifikasi jalur berlaku sama pada pemanggilannya. Kelas gabungan yang memuat dua salinan juga membawa ukuran memori dua kali lipat untuk bagian kelas puncaknya, sedangkan keadaan kedua salinan yang tidak sinkron menjadi sumber bug yang sulit dilacak. Kedua akibat itu hilang sekaligus pada bagian berikutnya, yaitu ketika kelas dasarnya dijadikan virtual.
Kelas dasar virtual#
Kelas dasar virtual adalah kelas dasar yang diwariskan dengan kata kunci virtual, dan cara itu menyatukan kedua salinan menjadi satu subobjek yang dipakai bersama. Kata kuncinya ditulis pada setiap jalur pewarisan yang menuju kelas dasar virtual, sehingga compiler tahu bahwa kedua jalur harus berbagi subobjek yang sama. Program berikut memakai hierarki yang sama dengan penambahan kata kunci tersebut.
#include <iostream>
#include <string>
class Orang {
public:
std::string nama = "(tanpa nama)";
Orang() {
std::cout << "Subobjek Orang dibangun\n";
}
};
class Mahasiswa : public virtual Orang {};
class Asisten : public virtual Orang {};
class AsistenPraktikum : public Mahasiswa, public Asisten {};
int main() {
AsistenPraktikum ap;
ap.nama = "Dewi"; // tidak ambigu: hanya ada satu subobjek Orang
std::cout << "via Mahasiswa: " << ap.Mahasiswa::nama << "\n";
std::cout << "via Asisten : " << ap.Asisten::nama << "\n";
return 0;
}Subobjek Orang dibangun
via Mahasiswa: Dewi
via Asisten : DewiSatu baris Subobjek Orang dibangun menunjukkan subobjeknya hanya ada satu, dan kini penulisan ap.nama sah tanpa kualifikasi karena hanya ada satu kandidat. Kata kunci virtual pada pewarisan ini tidak berhubungan dengan fungsi virtual meski tulisannya sama, karena yang diatur adalah keunikan subobjek kelas dasarnya. Aturan konstruksinya berubah pula, yaitu kelas dasar virtual dibangun oleh kelas paling turunan. Apabila Orang memiliki constructor berparameter, hanya AsistenPraktikum yang boleh menginisialisasinya, dan inisialisasi serupa di kelas tengah diabaikan. Biayanya berupa implementasi yang biasanya menambah lapisan penunjuk agar subobjek tunggal dapat ditemukan dari kedua jalur, sehingga aksesnya sedikit lebih mahal dan mekanisme internalnya lebih rumit saat debugging.
Pertimbangan pemakaiannya#
Pewarisan ganda yang membawa data member sebaiknya dipakai secukupnya, karena masalah diamond, aturan konstruksi yang lebih rumit, dan biaya kelas dasar virtual menyusul segera setelah hierarkinya bertambah. Pola yang lazim dan jauh lebih mudah dikelola adalah pewarisan ganda antarmuka, yaitu beberapa kelas dasar yang hanya berisi pure virtual function tanpa data member, sehingga masalah diamond jarang muncul dan penyelesaiannya ringan bila muncul. Bentuk kecil dari pola ini dikenal sebagai mixin, yaitu kelas kecil yang menambahkan satu kemampuan tertentu, dan ukurannya membuat kemampuan tersebut mudah diaudit.
Pustaka standar memakai pola ini pada std::basic_iostream yang mewarisi std::basic_istream dan std::basic_ostream, dengan std::basic_ios sebagai kelas dasar virtualnya. Sebagai pedoman praktis, pertimbangkan komposisi terlebih dahulu setiap kali dua kelas dasar membawa data. Simpan pewarisan ganda untuk kasus penggabungan antarmuka atau hierarki yang memang dirancang sejak awal untuk itu. Penilaian antara pewarisan dan komposisi dibahas lebih lanjut pada topik penutup modul ini.
Kesalahan umum#
Kesalahan berikut paling sering muncul ketika hierarki diamond ditulis pertama kali.
- Melupakan kata kunci
virtualpada salah satu jalur pewarisan. Salinannya tetap ada dua karena kelas dasar virtual ditentukan per jalur, sehingga kedua jalur wajib menuliskannya. - Mengakses member kelas puncak tanpa kualifikasi jalur. Compiler menolaknya sebagai ambiguitas, dan perbaikan sementaranya adalah kualifikasi seperti
Mahasiswa::nama, sedangkan perbaikan sesungguhnya menjadikan kelas dasarnya virtual. - Mengira inisialisasi kelas dasar virtual yang ditulis di kelas tengah ikut dijalankan. Hanya inisialisasi dari kelas paling turunan yang dipakai, sehingga argumen constructor perlu ditulis di sana.
- Menganggap pewarisan ganda selalu bermasalah. Pewarisan ganda antarmuka tanpa data member tergolong aman dan dipakai pustaka standar, sehingga yang perlu dijaga hanyalah diamond yang membawa data.
Ringkasan#
- Pewarisan ganda mendaftarkan beberapa kelas dasar sekaligus, dan urutan penulisannya menentukan urutan konstruksi subobjeknya.
- Pada hierarki diamond tanpa kata kunci
virtual, kelas paling bawah memuat dua salinan kelas puncak yang saling asing dan aksesnya ambigu. - Kelas dasar virtual menyatukan salinan tersebut menjadi satu subobjek yang dibangun oleh kelas paling turunan, dengan biaya lapisan penunjuk tambahan.
- Pewarisan ganda paling aman pada penggabungan antarmuka tanpa data, sedangkan diamond dengan data sebaiknya dihindari atau diganti komposisi.
Referensi#
- Derived classes — cppreference — Aturan pewarisan di standar bahasa, termasuk ketentuan kelas dasar virtual.
- Multiple inheritance — LearnCpp — Sintaks pewarisan ganda beserta masalah ambiguitas yang menyertainya.
- Virtual base classes — LearnCpp — Cara kelas dasar virtual menyatukan salinan dan aturan konstruksinya.
- Inheritance — multiple and virtual inheritance — C++ FAQ — Bahasan FAQ mengenai masalah diamond dan pertimbangan pemakaiannya.