Class nyata jarang berdiri sendiri: sebuah Mobil punya Mesin, Baterai, dan empat Ban; sebuah RekeningBank menyimpan Pemilik dan daftar transaksi. Composition adalah hubungan “memiliki” (has-a) ini — satu class mendeklarasikan data member yang bertipe class lain, sehingga object besar tersusun dari bagian-bagian yang lebih kecil. Yang membuat composition praktis di C++ adalah kerjanya yang otomatis: kita tidak perlu membangun bagian secara manual, compiler menguruskannya setiap kali object tuan dibuat dan dihancurkan. Chapter ini membedah mekanisme itu, termasuk urutan pasti ketika bagian dibangun dan dilepas kembali.

Member yang Bertipe Class Lain #

Bentuk paling dasar dari composition adalah mendeklarasikan data member dengan tipe sebuah class yang sudah ada. Class Mobil di bawah memiliki member baterai bertipe Baterai dan mesin bertipe Mesin — bukan pointer ke keduanya, melainkan object yang utuh tinggal di dalam Mobil. Saat kita menulis Mobil mobil("Kancil", 88);, ukuran memori mobil mencakup semua bagiannya: string merk, Baterai, dan Mesin berdampingan dalam satu blok.

Hubungan ini sering digambarkan dengan diagram berikut — Mobil adalah keseluruhan, Baterai dan Mesin adalah bagian-bagiannya yang tidak hidup tanpa tuannya:

Perbedaan istilah yang layak dicatat: composition adalah hubungan has-a, sedangkan inheritance yang akan kita bahas pada Chapter berikutnya adalah hubungan is-a. Mobil memiliki Mesin, tetapi mobil bukanlah mesin. Mengingat kalimat uji “X punya Y” atau “X adalah Y” akan sangat membantu ketika kita harus memilih antara keduanya.

Constructor Member Dipanggil Otomatis #

Bagian yang paling sering mengejutkan pemula: constructor dari member class dipanggil oleh compiler secara otomatis, sebelum badan constructor class pembungkus dieksekusi. Kita tidak pernah menulis mesin.Mesin(88); — tidak ada sintaks seperti itu. Pemanggilan terjadi karena member tercantum di member initializer list; bila member tidak dicantumkan sama sekali, compiler tetap memanggilnya, kali ini dengan default constructor-nya (atau dengan in-class initializer yang tertulis di deklarasinya).

Konsekuensinya penting untuk didesain: sebuah class yang bertujuan menjadi bagian harus menyediakan constructor yang mudah dipanggil oleh tuannya, biasanya lewat member initializer list seperti : mesin(hp). Bila bagian tidak punya default constructor dan tidak ikut di-initializer list, kompilasi gagal dengan pesan “no matching function for call” yang sudah kita kenal dari Chapter Default Constructor.

Urutan Konstruksi dan Destruksi #

Urutan konstruksi member mengikuti urutan deklarasi di dalam class, bukan urutan penulisan di member initializer list. Aturan ini sama dengan yang kita buktikan pada Chapter Member Initializer List untuk tipe dasar, dan berlaku identik untuk member bertipe class. Pada Mobil, baterai dideklarasikan sebelum mesin, sehingga Baterai selalu dibangun lebih dulu — apa pun yang kita tulis di daftar inisialisasi.

Destruksi berjalan persis terbalik: badan destructor Mobil dieksekusi dulu, lalu member dilepas satu per satu dari yang terakhir dideklarasikan, dan terakhir merk. Kebalikan-berlapis ini menjamin bahwa bagian tidak pernah dilepas lebih dulu daripada yang menggunakannya. Mari kita lihat semuanya dalam satu program dengan jejak cetak pada setiap constructor dan destructor:

cpp
#include <iostream>
#include <string>

class Baterai {
public:
    Baterai() {
        std::cout << "  [Baterai] terpasang\n";
    }
    ~Baterai() {
        std::cout << "  [Baterai] dilepas\n";
    }
};

class Mesin {
private:
    int tenaga;
public:
    explicit Mesin(int hp) : tenaga(hp) {
        std::cout << "  [Mesin] " << hp << " HP dipasang\n";
    }
    ~Mesin() {
        std::cout << "  [Mesin] dilepas\n";
    }
    void jalankan() const {
        std::cout << "  [Mesin] berputar dengan tenaga " << tenaga << " HP\n";
    }
};

class Mobil {
private:
    std::string merk;
    Baterai baterai;   // komposisi: member 1, dibangun lebih dulu
    Mesin mesin;       // komposisi: member 2, dibangun sesudahnya
public:
    Mobil(const std::string& m, int hp) : merk(m), baterai(), mesin(hp) {
        std::cout << "  [Mobil] " << merk << " selesai dirakit\n";
    }
    ~Mobil() {
        std::cout << "  [Mobil] " << merk << " dibongkar\n";
    }
    void jalan() const {
        mesin.jalankan();
        std::cout << "  [Mobil] " << merk << " melaju\n";
    }
};

int main() {
    Mobil mobil("Kancil", 88);
    std::cout << "-- menjalankan mobil --\n";
    mobil.jalan();
    std::cout << "-- akhir main, bagian dirapikan otomatis --\n";
    return 0;
}

Output yang Dihasilkan:

text
  [Baterai] terpasang
  [Mesin] 88 HP dipasang
  [Mobil] Kancil selesai dirakit
-- menjalankan mobil --
  [Mesin] berputar dengan tenaga 88 HP
  [Mobil] Kancil melaju
-- akhir main, bagian dirapikan otomatis --
  [Mobil] Kancil dibongkar
  [Mesin] dilepas
  [Baterai] dilepas

Mari kita bedah:

  1. Baris 4-12 (class Baterai): bagian paling sederhana, hanya constructor dan destructor berjejak cetak. Tidak ada yang istimewa — setiap class biasa bisa menjadi bagian dari class lain tanpa persiapan apa pun.
  2. Baris 18 (explicit Mesin(int hp)): explicit mencegah konversi implisit dari angka ke Mesin; untuk constructor berargumen tunggal ini kebiasaan baik agar Mobil m = 88; tidak sah diam-diam.
  3. Baris 32-33 (deklarasi member): inilah komposisi — Baterai baterai; dan Mesin mesin; dideklarasikan sebagai data member biasa. Keduanya hidup di dalam setiap Mobil, tanpa pointer dan tanpa alokasi terpisah.
  4. Baris 35 (member initializer list): baterai(), mesin(hp) menentukan constructor bagian mana yang dipanggil dengan argumen apa. Urutan penulisannya kebetulan sesuai deklarasi — jika dibalik, compiler memberi peringatan -Wreorder, tetapi urutan konstruksi tetap ditentukan deklarasi.
  5. Baris 48 (Mobil mobil("Kancil", 88)): tiga baris pertama output adalah bukti urutannya — Baterai, lalu Mesin, baru badan constructor Mobil mencetak “selesai dirakit”. Kita tidak memanggil satu pun constructor bagian secara manual.
  6. Baris 42 (mesin.jalankan()): method Mobil mendelegasikan sebagian tugasnya ke bagiannya sendiri. Inilah gaya khas desain berbasis komposisi: tuan mengatur, bagian menangani detail.
  7. Baris 51 (akhir main()): mobil keluar dari scope. Tiga baris terakhir output menunjukkan destruksi terbalik — badan ~Mobil() dulu, kemudian Mesin, terakhir Baterai.

Kapan Composition menjadi Pilihan #

Composition adalah alat default ketika satu class membutuhkan layanan class lain, tanpa hubungan “jenis yang sama”. RekeningBank yang menyimpan daftar Transaksi, Layar yang memiliki Buffer, Keranjang yang berisi Produk — semuanya komposisi, dan mayoritas class dalam kode C++ nyata tersusun seperti itu. Keuntungannya: hubungan longgar, bagian mudah diganti, dan setiap class dapat diuji sendiri-sendiri.

Satu hal yang belum kita sentuh: menyimpan bagian lewat pointer atau reference agar bagian bisa berganti saat program berjalan, atau dibagi antar object. Itu menuntut pemahaman alokasi memori yang tuntas — jalurnya kita susun di Section Manajemen Memori. Untuk sekarang, komposisi lewat member langsung sudah menutup mayoritas kebutuhan desain.

Chapter berikutnya baru membuka babak kedua OOP: inheritance, di mana class tidak lagi memiliki class lain, melainkan mewarisinya — dan urutan konstruksi yang baru saja kita pelajari akan kembali muncul dengan satu tambahan penting di depannya.

Rujukan resmi: Non-static data members (cppreference) , Constructor (cppreference) .