Mental model C++

C++ sebagai bahasa multiparadigma dengan filosofi zero-overhead dan artinya bagi penulisan kode.

Daftar isi

Cara berpikir saat menulis C++ berbeda dari bahasa yang dijalankan lewat interpreter, dan perbedaan itu bukan soal sintaks, melainkan soal mental model. Topik ini menetapkan dua gagasan yang menjadi dasar model tersebut, yaitu C++ sebagai bahasa multiparadigma dan filosofi zero-overhead yang menyertainya. Setelah topik ini dibaca, bentuk kode C++ yang terlihat rumit pada pandangan pertama dapat dijelaskan dari dua gagasan itu, dan keputusan desain kecil seputar tipe, memori, dan biaya eksekusi menjadi lebih mudah diambil.

Empat paradigma dalam satu bahasa#

C++ tidak menuntut satu gaya penulisan tunggal. Empat paradigma hidup berdampingan dalam bahasa yang sama. Gaya prosedural menyelesaikan pekerjaan melalui fungsi bebas dan struktur kontrol, sedangkan pemrograman berorientasi objek menggabungkan data dan perilaku ke dalam kelas (class). Pemrograman generik menulis algoritma sekali melalui template, lalu compiler menerapkannya untuk setiap tipe yang diperlukan.

Gaya fungsional menyumbang lambda, yaitu operasi tanpa nama yang dapat ditulis di tempat dan dikirim seperti nilai. Pemilihan gaya diserahkan pada masalah yang dihadapi. Kode nyata biasanya mencampurkan semuanya sesuai kebutuhan, dan program berikut menunjukkan keempatnya dalam satu berkas.

cpp
#include <iostream>
#include <vector>

// Gaya prosedural: fungsi bebas mengolah data yang diterimanya.
int total(const std::vector<int>& daftar) {
    int hasil = 0;
    for (int angka : daftar) {
        hasil += angka;
    }
    return hasil;
}

// Gaya berorientasi objek: data dan perilaku digabungkan dalam satu tipe.
class SensorSuhu {
public:
    void ukur(double celsius) { suhu = celsius; }
    double baca() const { return suhu; }
private:
    double suhu = 0.0;
};

// Gaya generik: template bekerja untuk banyak tipe tanpa ditulis ulang.
template <typename T>
T nilaiTerbesar(T a, T b) {
    return (a > b) ? a : b;
}

int main() {
    std::cout << total({10, 24, 100}) << '\n';

    SensorSuhu sensor;
    sensor.ukur(28.5);
    std::cout << sensor.baca() << '\n';

    std::cout << nilaiTerbesar(10, 24) << '\n';
    std::cout << nilaiTerbesar(1.5, 0.75) << '\n';

    // Gaya fungsional: lambda berupa operasi tanpa nama.
    auto duaKali = [](int x) { return 2 * x; };
    std::cout << duaKali(24) << '\n';

    return 0;
}
text Keluaran
134
28.5
24
1.5
48

Fungsi total mewakili gaya prosedural karena hanya mengolah data yang diterimanya. Kelas SensorSuhu mewakili gaya berorientasi objek karena nilai suhu dan cara membacanya digabungkan dalam satu tipe. Template nilaiTerbesar bekerja untuk int maupun double tanpa ditulis ulang, sedangkan lambda duaKali menunjukkan operasi sekali pakai yang ditulis langsung di tempatnya. Kode C++ sehari-hari biasanya berupa campuran keempatnya sesuai kebutuhan.

Filosofi zero-overhead#

Filosofi zero-overhead adalah rumusan Bjarne Stroustrup tentang desain C++: abstraksi tidak boleh menambah biaya dibanding kode setara yang ditulis manual. Apabila sebuah fitur tidak dipakai, tidak ada biaya yang dibayarkan, dan apabila fitur itu dipakai, tidak ada cara menulis tangan yang menghasilkan kode lebih efisien. Prinsip inilah yang membuat abstraksi tingkat tinggi seperti container dan template tetap dipakai di kode berperforma tinggi.

Contoh yang mudah diamati adalah tipe buatan sendiri. Struct tanpa fungsi virtual tidak membawa data apa pun di luar anggotanya, dan ukurannya dapat diperiksa dengan operator sizeof. Program berikut membandingkan ukuran struct Titik dengan ukuran dua angka double yang menjadi anggotanya.

cpp
#include <iostream>

struct Titik {
    double x;
    double y;
};

int main() {
    std::cout << "Ukuran Titik: " << sizeof(Titik) << " byte\n";
    std::cout << "Ukuran dua double: " << 2 * sizeof(double) << " byte\n";
    return 0;
}
text Keluaran
Ukuran Titik: 16 byte
Ukuran dua double: 16 byte

Pada platform 64-bit yang umum, satu double menempati 8 byte, sehingga dua angka membutuhkan 16 byte. Struct Titik menempati jumlah yang persis sama, artinya abstraksi struct tidak menambah pungutan apa pun di luar datanya. Sebaliknya, apabila Titik diberi fungsi virtual, ukurannya bertambah satu pointer untuk tabel pemanggilan. Biaya tersebut hanya muncul karena fiturnya dipakai, dan persis di titik itulah filosofi zero-overhead bekerja.

Konsekuensi bagi penulisan kode#

Konsekuensi pertama adalah tidak adanya garbage collector, yaitu mekanisme pembersihan memori otomatis yang berjalan di belakang layar. Masa hidup objek ditangani melalui konstruksi dan destruksi yang deterministik. Objek yang dibuat di dalam sebuah fungsi dihancurkan tepat ketika eksekusi keluar dari lingkupnya, tanpa menunggu pembersihan berkala. Pada saat destruksi itulah sumber daya seperti memori dan berkas dilepaskan. Pola pengikatan pembersihan sumber daya pada masa hidup objek semacam ini disebut RAII (Resource Acquisition Is Initialization) dan menjadi tulang punggung pengelolaan sumber daya di C++.

Konsekuensi kedua adalah kemunculan biaya di dalam kode, bukan di runtime. Setiap penyalinan objek besar, setiap alokasi memori dinamis, dan setiap panggilan fungsi virtual adalah biaya yang terlihat pada baris yang menimbulkannya. Bahasa lain sering menyembunyikan biaya serupa di balik runtime-nya. Akibatnya, membaca kode C++ menuntut perhatian pada tipe, kepemilikan, dan masa hidup objek, dan keterampilan tersebut lebih menentukan daripada sekadar menghafal sintaks.

Konsekuensi ketiga menyangkut penyebaran program. Hasil kompilasi berupa kode mesin yang berlaku untuk arsitektur dan sistem operasi target kompilasi. Portabilitas diperoleh dengan mengompilasi ulang kode sumber yang sama pada platform lain, bukan dengan menjalankan kode sumber lewat interpreter. Cara kerja ini sejalan dengan penggunaan C++ pada lingkungan yang beragam, mulai dari server hingga mikrokontroler.

Kesalahan umum#

  1. C++ dianggap cepat dengan sendirinya. Kemampuan menghasilkan kode mesin bukan jaminan performa. Penyalinan data yang tidak perlu, pilihan struktur data yang keliru, atau alokasi memori di dalam perulangan dapat membuat program C++ lebih lambat daripada implementasi bahasa lain yang lebih sederhana. Penyebabnya, filosofi zero-overhead disalahartikan sebagai janji otomatis. Perbaikannya berupa penanganan performa sebagai keputusan desain, yaitu memilih algoritma dan struktur data yang tepat lebih dahulu, mengukurnya, lalu mengoptimasi bagian yang terbukti lambat.
  2. Zero-overhead dianggap berarti tanpa biaya. Filosofi tersebut menyatakan abstraksi tidak menambah pungutan di luar fitur yang dipakai, bukan berarti semua operasi gratis. Alokasi memori dinamis, penyalinan string, dan panggilan fungsi virtual tetap berbiaya nyata. Penyebabnya adalah pembacaan slogan tanpa konteksnya. Perbaikannya berupa kebiasaan menghubungkan setiap fitur dengan biayanya, sehingga keputusan penulisan kode berpijak pada biaya yang diketahui.

Ringkasan#

  • C++ menampung paradigma prosedural, berorientasi objek, generik, dan fungsional dalam satu bahasa, dan pemilihan gayanya mengikuti masalah yang dihadapi.
  • Filosofi zero-overhead menyatakan abstraksi tidak menambah biaya di luar fitur yang benar-benar dipakai.
  • Tidak ada garbage collector, sehingga masa hidup objek dikelola secara deterministik melalui konstruksi dan destruksi yang menopang pola RAII.
  • Kebebasan pengendalian memori menuntut kepatuhan pada aturan bahasa, karena pelanggarannya menghasilkan undefined behavior.

Referensi#