Sejauh ini setiap class turunan kita punya satu class dasar. C++ tidak membatasi jumlahnya: class C : public A, public B membuat satu class mewarisi dua dasar sekaligus, lengkap dengan data dan method dari keduanya. Kemampuan ini disebut multiple inheritance, dan ia fitur yang jarang dimiliki bahasa berorientasi objek lain — Java dan C# menolaknya untuk class, Python mengizinkan dengan aturan resolusi sendiri. Daya tariknya nyata, tetapi ia membawa jebakan klasik bernama diamond problem yang harus dipahami sebelum kita berani memakainya. Chapter ini membahas bentuk dasarnya, urutan konstruksinya, lalu menuntaskan masalah diamond bersama virtual inheritance.

Mewarisi Dua Dasar Sekaligus #

Sintaksnya persis seperti single inheritance dengan daftar dasar yang dipisah koma: class MahasiswaKaryawan : public Pekerja, public Pelajar. Object hasilnya berisi dua subtipe dasar berdampingan — bagian Pekerja lalu bagian Pelajar — ditambah member milik class itu sendiri. Semua method dan data dari kedua dasar langsung bisa dipakai, dan class turunan bebas menambahkan member baru seperti biasa.

Urutan konstruksi mengikuti urutan penulisan daftar dasar, bukan urutan di member initializer list: Pekerja dibangun dulu, kemudian Pelajar, baru badan constructor turunan. Destruksinya berjalan terbalik. Program berikut membuktikan urutan itu dengan jejak cetak, sekaligus menunjukkan bahwa member kedua dasar hidup damai dalam satu object:

cpp
#include <iostream>
#include <string>

class Pekerja {
public:
    std::string perusahaan;
    Pekerja() {
        std::cout << "1. Pekerja dibangun\n";
    }
    void bekerja() const {
        std::cout << "bekerja di " << perusahaan << "\n";
    }
};

class Pelajar {
public:
    std::string kampus;
    Pelajar() {
        std::cout << "2. Pelajar dibangun\n";
    }
    void belajar() const {
        std::cout << "belajar di " << kampus << "\n";
    }
};

// Multiple inheritance: daftar dasar dipisah koma,
// urutannya menentukan urutan konstruksi
class MahasiswaKaryawan : public Pekerja, public Pelajar {
public:
    MahasiswaKaryawan() {
        std::cout << "3. MahasiswaKaryawan dibangun\n";
    }
};

int main() {
    MahasiswaKaryawan mk;
    mk.perusahaan = "PT Maju Jaya";
    mk.kampus = "Universitas Cakrawala";
    mk.bekerja();
    mk.belajar();
    return 0;
}

Output yang Dihasilkan:

text
1. Pekerja dibangun
2. Pelajar dibangun
3. MahasiswaKaryawan dibangun
bekerja di PT Maju Jaya
belajar di Universitas Cakrawala

Mari kita bedah:

  1. Baris 4-13 (class Pekerja): dasar pertama, hanya punya constructor berjejak dan satu method. Tidak ada persiapan khusus untuk diwarisi dua kali — class biasa apa pun bisa menjadi dasar ganda.
  2. Baris 15-24 (class Pelajar): dasar kedua dengan struktur identik. Keduanya sengaja berbeda nama member (perusahaan vs kampus) supaya tidak ada tumpang tindih nama.
  3. Baris 27 (: public Pekerja, public Pelajar): daftar dasar dipisah koma; setiap dasar membawa mode aksesnya sendiri. Urutan di sini yang menentukan: baris pertama output menunjukkan Pekerja dibangun sebelum Pelajar.
  4. Baris 35-39 (di main()): satu object mk menjawab dua antarmuka sekaligus — mk.bekerja() dari dasar pertama, mk.belajar() dari dasar kedua. Kedua data member pun bisa diisi langsung tanpa konversi apa pun.

Diamond Problem: Satu Kakek, Dua Jalur #

Masalah muncul ketika dua dasar itu sendiri mewarisi satu dasar yang sama. Klasiknya seperti ini: Burung : public Hewan dan Ikan : public Hewan, lalu Itik : public Burung, public Ikan. Itik mewarisi Hewan melalui dua jalur berbeda, membentuk bentuk berlian (diamond) pada pohon inheritance:

Tanpa tindakan apa pun, Itik berisi dua subtipe Hewan yang berbeda — satu ikut Burung, satu lagi ikut Ikan. Konsekuensinya menjengkelkan: memanggil itik.makan() membingungkan compiler karena ada dua Hewan::makan() yang sama-sama valid, dan data nama pun dobel. Compiler menolak dengan pesan yang jujur:

text Error kompilasi: panggilan ambigu pada diamond
error: request for member 'makan' is ambiguous
note: candidates are: 'void Hewan::makan() const'
note:                 'void Hewan::makan() const'

Perhatikan pesan “candidates are” mencantumkan dua kandidat yang tampilannya sama persis — itulah tanda ada dua subtipe dasar yang saling menutupi. Cast eksplisit seperti itik.Burung::makan() bisa memilih jalur, tetapi dua salinan Hewan tetap ada: dua memori, dua state, dua kesempatan untuk tidak sinkron.

Virtual Inheritance: Berbagi Satu Dasar #

C++ memberi solusi resmi: menandai inheritance dasar yang sama dengan virtual. Menulis class Burung : virtual public Hewan adalah berjanji bahwa di seluruh pohon di bawahnya, hanya satu subtipe Hewan yang akan dibagikan. Ketika Itik mewarisi Burung dan Ikan yang keduanya virtual terhadap Hewan, dua jalur itu bertemu di satu Hewan yang sama — diamondnya tertutup rapi.

Ada dua aturan praktis yang menyertai virtual inheritance. Pertama, virtual base selalu dibangun paling awal oleh class paling turunan (most-derived) — pada contoh di bawah, jejak cetak Hewan muncul sebelum Burung dan Ikan, dan hanya satu kali. Kedua, class paling turunan yang bertanggung jawab memanggil constructor virtual base secara langsung; bila tidak dicantumkan, default constructor virtual base yang dipakai. Mari kita lihat versi tertutupnya:

cpp
#include <iostream>
#include <string>

class Hewan {
public:
    std::string nama;
    Hewan() : nama("Si Putih") {
        std::cout << "Hewan dibangun\n";
    }
    void makan() const {
        std::cout << nama << " sedang makan\n";
    }
};

// virtual public Hewan: berjanji hanya SATU Hewan akan ada
// di pohon inheritance di bawah class ini
class Burung : virtual public Hewan {
public:
    Burung() {
        std::cout << "  Burung dibangun\n";
    }
};

class Ikan : virtual public Hewan {
public:
    Ikan() {
        std::cout << "  Ikan dibangun\n";
    }
};

// Itik mewarisi dua dasar yang sama; berkat virtual inheritance
// keduanya berbagi satu subtipe Hewan yang sama
class Itik : public Burung, public Ikan {
public:
    Itik() {
        std::cout << "  Itik dibangun\n";
    }
};

int main() {
    Itik itik;
    itik.makan();   // tanpa virtual: ambigu; dengan virtual: satu jalur jelas
    return 0;
}

Output yang Dihasilkan:

text
Hewan dibangun
  Burung dibangun
  Ikan dibangun
  Itik dibangun
Si Putih sedang makan

Mari kita bedah:

  1. Baris 16 dan 23 (virtual public Hewan): kata virtual ditulis pada dua class menengah — inilah yang menyatakan “siap berbagi subtipe Hewan dengan jalur lain”. Menandai hanya satu dari keduanya tidak cukup; dua jalur harus sama-sama virtual.
  2. Baris 31 (class Itik : public Burung, public Ikan): class paling turunan tidak perlu menulis apa pun khusus — berkat janji di atas, kedua dasar menunjuk satu subtipe Hewan yang sama.
  3. Baris pertama output: Hewan dibangun paling awal dan sekali saja — bukti bahwa virtual base berpindah ke posisi terdepan dalam urutan konstruksi, menggeser Burung dan Ikan ke belakangnya.
  4. Baris 40 (itik.makan()): panggilan yang pada versi tanpa virtual memicu error ambigu kini lolos dan menjawab satu jalur. Data nama juga tunggal: mengubahnya lewat Burung atau Ikan menyentuh variabel yang sama.

Sebagai penutup, catatan keseimbangan: multiple inheritance yang aman biasanya adalah yang dasar-dasarnya benar-benar independen (dua peran berbeda), atau yang digabung dengan antarmuka murni — pola yang akan sering kita temui mulai Chapter Abstract Classes. Diamond yang dipaksakan dengan virtual inheritance adalah alat yang sah, tetapi ia alat pemadam kebakaran, bukan perkakas harian.

Satu fasilitas kecil yang menyertai pewarisan — termasuk yang berlipat — layak dicatat: inherited constructors. Dengan menulis using Base::Base; di dalam class turunan, seluruh constructor milik Base menjadi dipakai untuk membuat turunan tanpa kita menulis ulang satu per satu. Pada pewarisan tunggal fasilitas ini menghemat penulisan constructor penerus; pada multiple inheritance, setiap dasar yang ditarik lewat using menyumbang kumpulan constructornya sendiri, sehingga aturan kepemilikan (base mana yang menyediakan constructor apa) perlu tetap terbaca jelas. Kita cukup mengenal keberadaannya di sini — pemakaiannya paling berguna saat class turunan murni menambah perilaku tanpa menambah data.

Chapter berikutnya menyentuh mekanisme yang sudah sering kita dengar namanya sejak awal section ini: virtual functions, mesin di balik perilaku “object menentukan sendiri versi methodnya” yang menjadi jantung polymorphism.

Rujukan resmi: Derived classes (cppreference) , Virtual specifier (cppreference) .