Kebangkitan C++, Pelopor OOP

Perjalanan menjadi pejuang C++ memang tidak mudah. Banyak yang mencoba dan kandas ketika baru mulai. Banyak pula yang mencoba, menaklukkan masalah-masalah kecil, dan menggembar-gemborkan diri sebagai pejuang hebat. Banyak lagi yang mencukupkan diri dengan apa yang ada di depan mata, terjebak dalam dunia lama yang segera ditelan jaman. Dan, ada juga yang seperti saya, tersesat terombang-ambing dalam lautan dan badai stack overflow, memory leak, dan crash setiap harinya. Ini adalah sebuah peta jalan. Mereka yang membaca ini bisa jadi tidak pernah menginjakkan kaki di perahu C++. Bisa juga, mereka yang sedang tersesat pusaran air yang ganas. Apapun itu, sebuah peta jalan akan membantu.
Peta ini saya tulis dan saya gambar sesuai pengalaman saya. Ada yang dilebih-lebihkan, ada yang dipandang dengan kurang berimbang. Begitulah mata dan hati saya mengalami perjalanan-perjalanan dengan C++ ini. Yang jelas, pokoknya semua ini benar. Dan, mereka yang tidak percaya dengan peta ini adalah orang-orang yang akan tersesat.

Pada Jaman Dahulu …

punched card

Liat gambar di atas? Itu adalah sebuah punch card. Itulah “program executable” jaman dahulu.Yah, pada jaman dahulu, orang memprogram memakai punch card. Instruksinya pun dalam kode biner langsung. Terus orang menyederhanakan kode biner dengan mnemonic, jadilah assembly. Orang melewatkan kode assembly ke sebuah assembler. Dulu, setiap keluarga prosesor memiliki bahasanya sendiri-sendiri. Ada bahasa x86, ada bahasa ARM, ada bahasa SPARC, bahasa DEC, dan lain-lain. Programmer jaman dulu harus belajar lagi bahasa yang berbeda jika mereka berpindah mesin.

Kemudian, diciptakanlah bahasa C yang salah satu tujuan utamanya adalah untuk menjadi portable assembler. Bahasa C ini ibarat bahasa Inggris untuk keluarga prosesor yang berbeda-beda. Bahasa C akan diterjemahkan menjadi bahasa mesin oleh yang namanya compiler. Tiap keluarga prosesor / arsitektur (ISA - Instruction Set Architecture) membutuhkan compiler tersendiri untuk bahasa C. Write once, compile anywhere. C menjadi terobosan baru untuk dunia perkomputeran.

Warisan bahasa C ini melekat sangat kuat di C++. Hingga saat ini, sebagian besar konstruksi bahasa C bisa dicompile ke dalam C++. Ketika C++ modern berusaha meninggalkan warisan masa lalu dan menyambut masa depan, mereka tetap tidak bisa melepaskan masa lalu begitu saja. Saya selalu menyatakan diri sebagai programmer C++, bukan programmer C. Bukan juga programmer C/C++. Cara berpikir kedua jenis programmer ini berbeda, dan pendekatannya pun berlainan. Tanpa memahami warisan masa lalu bahasa C ini, kita tidak bisa menyambut masa depan C++ modern. Untuk itu, marilah kita lihat sifat-sifat dari bahasa C ini.

Transparan

Ketika diciptakan pertama kali, C ditulis untuk mesin DEC PDP 11. Waktu itu, CPU tidak serumit sekarang. Arsitektur hardware seakan-akan telanjang di hadapan programmer C. Konsep pointer dan array yang membingungkan itu juga akibat dari arsitektur hardware yang tembus pandang ini. Begitu juga hingga saat ini ketika anda memprogram C untuk mikrokontroler yang hanya punya SRAM + ROM. Semuanya ada di depan mata.

cpu microarchitecture

Namun, jika anda memprogram di Raspberry Pi atau di komputer desktop misalnya, maka ceritanya akan jadi jauh berbeda. Sebagai contoh, program C akan menganggap bahwa memori itu sebagai satu lemari penyimpanan yang sangat besar. Tapi, siapa tidak tau kalo ada yang namanya register, ada yang namanya L1 cache, L2 cache, L3 cache, DDR berkeping-keping, dan lain sebagainya. Begitu anda berurusan dengan mesin modern, apa yang dilakukan oleh program C anda tidak begitu jujur lagi. Dan, anda harus kembali terjun ke bahasa assembly untuk memaksa mesin anda jujur kepada anda. Misal, ketika berurusan dengan SIMD. Begitulah gambaran mental yang terbentuk di benak programmer C, terutama dari pemula hingga agak menengah. Mereka merasa memprogram secara low level, padahal nggak juga. Memprogram C adalah memprogram secara high level, cuman lebih bertele-tele saja.

Penuh Bahaya

Ketika C pertama kali diciptakan, belum ada sistem operasi yang jalan di bawahnya. Bahkan, bahasa C sendiri itu digunakan untuk menulis sistem operasi UNIX. Karena itulah, bahasa C ini inherently unsafe. Si programmer berurusan langsung dengan hardware. Perancang bahasa C beranggapan bahwa para programmer-nya benar-benar tahu apa yang mereka lakukan. Inilah mengapa fitur-fitur aneh semacam union ada. Atau, inilah mengapa tidak ada pengecekan terhadap panjang array dan pointer.

Bertele-tele

Dan, inilah sifat terakhir dari bahasa C, yaitu menitikberatkan kepada programmer yang ngomong kepada mesin, ketimbang programmer menyampaikan idenya seperti apa. Dan, yang begini ini adalah siksaan bagi pemikir abstrak seperti saya. Satu ide harus diungkapkan dalam 100 baris. Cara dan pola yang sama harus dilakukan berulang-ulang di bagian-bagian yang lain. Ibaratnya, kita ngomong ke orang idiot. Harus sangat bertele-tele baru dia mengerti. Harus lengkap baru bisa jalan. Straightforward iya. Tapi sama sekali tidak expressive dan tidak powerful. Tidak profound, istilah filosofisnya.

Sederhana

Dalam mengungkapkan sebuah gagasan bahasa yang sederhana dan bertele-tele adalah dua sisi dari satu keping uang logam. Ini ibarat Bahasa Indonesia dan Bahasa Inggris. Bahasa Indonesia lebih sederhana daripada bahasa Inggris. Sebagai contoh, “I left” dan “Saya sudah pergi”. Bahasa yang sederhana membutuhkan lebih banyak kata. Begitulah bahasa C. Memang sih, kesannya C itu jadi gampang. Akan tetapi, tidak begitu juga. C itu “easy to learn, hard to master”. Belajar sebentar cepat bisa. Yang susah adalah mengenali dan menaklukkan prosesornya. Inilah mengapa C disukai orang-orang yang sering ngoprek hardware. Soalnya program yang mereka bikin biasanya kecil, dan mereka lebih suka ngoprek hardware ketimbang ngoding ini itu banyak sekali. Mereka belajar C supaya hardware-nya jalan, bukan karena pengen menyelesaikan masalah dengan software.

C++, Sebuah Bahasa untuk Gen X

Kalo anda melihat TIOBE index dan menelusuri tiga bahasa yang paling populer di tahun 1990-an, anda akan menemukan bahwa salah satunya adalah C++. Alasannya tentu saja bukan karena C++ memang bagus. Alasannya adalah kecelakaan sejarah. Sebagaimana juga alasan mengapa TCP/IP menjadi fondasi infrastruktur internet dunia. Sebagaimana juga alasan mengapa PHP dan Javascript menjadi bahasa yang sangat populer untuk pengembangan web. Dan, akan sangat banyak contoh-contoh teknologi canggih yang menjadi terkemuka bukan karena seberapa bagusnya teknologi tersebut memecahkan masalah, tapi seberapa diterimanya teknologi tersebut di kalangan para pengusungnya. Inilah salah satu bukti bahwa programmer tidak kehilangan kemanusiaannya. Bahwa mereka bukanlah makhluk yang sepenuhnya rasional, tapi masih bisa baper dan ngawur.

tiobe-index-very-long-term-history

Berkembangnya Object Oriented Programming

Istilah object-oriented programming pertama kali dicetuskan oleh Alan Kay, ketika beliau terinspirasi dari cara kerja sel di dalam makhluk hidup. Masing-masingnya berdiri sendiri, tapi saling berkomunikasi untuk membentuk satu sistem yang utuh. Contohnya adalah sistem pernafasan atau sistem pencernaan. Eyang Alan kemudian berfikiran bahwa sebuah sistem software yang rumit bisa saja disusun dari bagian-bagian kecil yang kemudian dinamainya sebagai “object”. Kita liat apa kata Eyang Alan soal ini:

OOP to me means only messaging, local retention and protection and hiding of state-process, and extreme late-binding of all things.

Dan, yang lebih mengejutkan, beliau bilang…

I invented the term Object-Oriented, and I can tell you I did not have C++ in mind.

Kalo kita menelisik lebih lanjut, ini sebenarnya wajar-wajar saja. Smalltalk yang adalah bahasa kesayangan Eyang Alan dan C++ yang menjadi bahasan pokok kita sama-sama terinspirasi dari bahasa Simula 67. Alan Kay adalah yang mencetuskan pertama kali istilah OOP, dan bahasa-bahasa lain pinjem istilah ini, tapi tidak pernah dikembalikan. Akibatnya, OOP ala C++, Java, dan C# adalah OOP yang kita kenal sekarang ini sebagai OOP yang baku.

Mengesampingkan urusan sejarah, sebenarnya masalah apa sih yang berusaha diselesaikan oleh OOP di C++ ini? OK lah, kita lihat penjelasan di bawah ini:

1. Encapsulation

Bad programmers worry about the code. Good programmers worry about data structures and their relationships. (Linus Torvalds)

Show me your code and conceal your data structures, and I shall continue to be mystified. Show me your data structures, and I won’t usually need your code; it’ll be obvious. (Eric S. Raymond)

Smart data structures and dumb code works a lot better than the other way around.(Eric S. Raymond)

Para begawan ilmu koding sudah mengeluarkan sabdanya. Nah, sekarang mari kita coba ingat struktur data yang dulu mula-mula kita pelajari. Apa sajakah mereka?

  1. Array
  2. Linked List
  3. Stack
  4. Queue
  5. Map

Masing-masing struktur data ini punya beberapa operasi untuk memanipulasi / mengubah struktur data tersebut. Misalnya saja:

  1. Insert
  2. Remove
  3. Sort
  4. Random Access
  5. Sequential Access

Sebelum ada C++, kira-kira beginilah kalo kita mau menggunakan stack:

int_stack_t* a_stack = stack_create();
stack_push(a_stack, 2);
stack_push(a_stack, 3);
stack_push(a_stack, 5);
stack_push(a_stack, 7);

while (a_stack != NULL)
{
    printf("stack item=%d", stack_pop(a_stack));
}

stack_destroy(a_stack);

Setelah ada C++, jadinya begini:

stack<int> a_stack;
a_stack.push(2);
a_stack.push(3);
a_stack.push(5);
a_stack.push(7);

while (!a_stack.empty())
{
    cout << "stack item=" << a_stack.top() << endl;
    a_stack.pop();
}

Itulah alasan paling mendasar adanya OOP, yaitu untuk menyatukan struktur data beserta fungsi-fungsi yang melekat padanya. Inilah yang dinamakan dengan encapsulation. Encapsulation juga bermanfaat untuk membatasi manipulasi struktur data hanya melalui fungsi yang sesuai. Coba anda bayangkan jika suatu struktur data, Tree misalnya, bebas diobrak-abrik tanpa ada batasan. Akibatnya adalah akan ada suatu saat di mana struktur data tersebut menjadi tidak konsisten.

Begini contohnya:

a_stack->next = a_stack;

Begitu instruksi di atas dieksekusi, maka yang tadinya stack akan jadi circular buffer. Dan, taukah anda? Semua buntut stack akan tidak bisa diakses dan bakalan jadi memory leak. Tidak hanya itu saja, sekarang tidak ada cara untuk menghapus stack. Ketika fungsi stack_destroy(a_stack) dipanggil, jadinya infinite loop. Karena biasanya, stack_destroy itu diimplementasikan seperti ini:

void stack_destroy(int_stack_t* a_stack)
{
    while (a_stack)
    {
        int_stack_t* node_to_destroy = a_stack;
        a_stack = a_stack->next;
        free(node_to_destroy);
    }
}

Nah, dengan adanya OOP, yang seperti ini tidak akan terjadi. Kita bisa menyembunyikan field-field dari stack jadi private, sehingga tidak ada cerita orang salah pakai stack. Memang sih, contoh di atas tadi terlalu karikatur. Akan tetapi, ketika anda berurusan dengan struktur data di codebase yang besar, yang mana suatu variabel diakses dari beberapa bagian kode anda, barulah hal ini jadi masuk akal. Itulah kenapa variabel global itu keliatannya gampang di awal, tapi begitu kode kita membengkak dan masalah yang dihadapi semakin rumit, semakin pusing lah kita karena kode untuk mengurusi struktur data bertebaran di mana-mana, dan masing-masing bagian kode punya cara sendiri untuk berurusan dengan struct yang sama.

2. Modularity

Efek samping dari menyatukan data dan fungsi, akhirnya programmer bisa memecah-mecah software-nya menjadi beberapa modul. Jangan salah lho ya, sejak sebelum C++ orang-orang sudah terbiasa memecah fungsi yang panjang menjadi fungsi-fungsi lain yang pendek-pendek. Begitulah, modularitas bukan soal memecah fungsi saja, melainkan juga memecah data. Akhirnya, programmer bisa mengurai kerumitan variabel global dengan variabel-variabel lokal di masing-masing modul.

Ini penting! Konsep ini terkenal dengan istilah devide et impera. Artinya, membagi masalah yang rumit jadi kecil-kecil, supaya masing-masingnya bisa diselesaikan dengan mudah. Dengan demikian, satu pekerjaan besar bisa dibagi-bagi untuk banyak orang. Mereka cuma perlu janjian input dan outputnya saja, sementara masing-masing menyimpan variabel-nya sendiri-sendiri. Mereka tidak harus ngomong-ngomong ke yang lain variabel-variabel di dalam modul merek. Yang penting input dan outputnya benar. Karena itu, jangan mengkhianati perjuangan para pelopor OOP dengan banyak memakai Singleton. Itu goblok kuadrat namanya.

3. Interface

Interface bukan istilah resmi yang diakui dalam C++. Interface di C++ resmi dikenal dengan pure virtual class. Akan tetapi, konsep ini sangat penting dalam OOP klasik. Konsep inilah yang membuat sebuah software berbasis OOP jadi powerful, karena dia memungkinkan terjadinya dua hal:

  • Adanya perubahan perilaku program secara dinamis saat program berjalan, tanpa suatu modul tahu bahwa modul yang lain sudah berubah

    Ini lazim dinamakan dengan polymorphism. Polymorphism menyederhanakan dan menghilangkan if else, di saat-saat di mana penggunaannya menjadi tidak praktis lagi. Kode-kode yang tersebar di mana-mana dengan if else yang selalu sama, akhirnya semuanya di masukkan ke dalam klas-klas masing-masing. Ditaruhlah interface di depan klas-klas itu, sehingga sekarang kode yang baru terasa lebih mudah dibaca.

  • Pembatasan antara dua komponen software supaya tidak saling menyeberang ke komponen yang lain

    Hal ini membuat dua modul yang saling bekerja sama untuk bisa bertumbuh bersama tanpa sering-sering merusak dan mengganggu yang lain. Interaksi satu modul dengan modul lain dicukupkan hanya melalui interface saja. Karena itu, interface yang bagus adalah interface yang sedikit dan secukupnya saja. Dan, jangan malu-malu untuk membuat banyak interface. Apalagi, kalo memang masing-masing interface punya kebutuhan yang beda-beda.

Generic programming

Di saat yang bersamaan OOP dikembangkan di atas C oleh Bjarne Stroustrup, Alexander Stepanov menjelajahi pendekatan baru yang dinamakan dengan generic programming. Generic programming berarti membuat suatu algoritma yang bisa diterapkan tipe yang berbeda-beda pada saat akan dipakai. “Algoritma” ini sudah berbentuk source code dan tidak perlu berubah sama sekali ketika akan dipakai. Sehingga, algoritma ini dikatakan generik. Algoritma inilah yang disebut sebagai template di C++. Komite C++ mengumpulkan template-template ini dalam standard library mereka, yang kemudian dinamakan sebagai “Standard Template Library”.

Saudara kembar inheritance

Tidak banyak yang tahu jika template itu ternyata menyelesaikan permasalahan yang sama dengan inheritance, tapi dengan pendekatan yang berbeda. Bisa dikatakan, generic programming adalah saudara kembar object oriented programming. Bagaimana bisa begitu? Marilah kita lihat bagaimana generic programming dan object oriented programming menyelesaikan permasalahan sorting.

class IComparable {
public:
    virtual int compare(IComparable* c) = 0; //<0 smaller, 0 equal, >0 greater
};

class Employee : public IComparable {
public:
    string name;
    unsigned age;
    string rank;

    //oop
    int compare(IComparable* c) {
        return this->name.compare(dynamic_cast<Employee*>(c)->name);
    }

    //generic
    int compare(Employee* c) {
        return this->name.compare(c->name);
    }
};

void sort_oop(IComparable** elems, unsigned size, bool ascending) {
    for (int i = 0; i < size - 1; i++) {
        for (int j = i + 1; j < size; j++) {
            if (ascending ^ (elems[i]->compare(elems[j]) < 0)) {
                IComparable* temp = elems[i];
                elems[i] = elems[j];
                elems[j] = temp;
            }
        }
    }
}

template <typename T>
void sort_generic(T** elems, unsigned size, bool ascending) {
    for (int i = 0; i < size - 1; i++) {
        for (int j = i + 1; j < size; j++) {
            if (ascending ^ (elems[i]->compare(elems[j]) < 0)) {
                T* temp = elems[i];
                elems[i] = elems[j];
                elems[j] = temp;
            }
        }
    }
}

Nah, mirip sekali bukan? Yah, walaupun ada beberapa perbedaan. Template tidak mensyaratkan batasan untuk tipe data yang akan dimasukkan ke template tersebut. Akibatnya, seringkali compile error yang dihasilkan jika tipe data salah susah dimengerti. Ini akan diatasi di C++ modern dengan adanya “Concepts”. Sementara itu sebaliknya, dengan OOP, Employee harus dianggap sebagai IComparable di dalam algoritma sort, sehingga di fungsi compare, ada downcasting paksa di sana. Bisa saja kita bikin array dengan isi IComparable yang tidak setipe. Hasilnya, saat runtime sort_oop akan gagal. Tapi, secara konsep, keduanya menyelesaikan masalah yang sama.

Non type template parameter

Generic programming ini ternyata bisa dikembangkan lebih jauh, tidak hanya kepada tipe data saja. Template parameter di C++ bisa berupa:

  • Integer
  • Enum
  • Pointer (baik ke fungsi atau ke object)

Apa gunanya? Dulu, saya pake ini untuk bikin circular buffer berbasis array


//dengan begini, queue bebas mau dialokasikan di stack atau heap
template <typename T, size_t N>
class circular_queue {
private:
    T buffer[N];
//...
//...
};

//pas dipakai
circular_queue<int, 25> queue;

Template metaprogramming

Canggihnya lagi, ternyata template itu Turing complete. Apa maksudnya Turing complete? Maksudnya, template itu jadi kayak bahasa pemrograman tersendiri. Dia bisa mengeksekusi kode apapun, misalnya menghitung faktorial, PI, atau konstanta Matematika lainnya pas compile time.

//menghitung faktorial secara rekursif menggunakan template
template <unsigned int n>
struct factorial {
	enum { value = n * factorial<n - 1>::value };
};

//ini jangkar rekursifnya, diimplementasikan dengan "specialization"
template <>
struct factorial<0> {
	enum { value = 1 };
};

//pas dipakai
cout << factorial<6>::value << endl;

Potongan kode di atas adalah contoh yang paling sederhana dari template metaprogramming. Template metaprogramming merupakan topik kontroversial C++ di masa tahun 2000-an, di mana beberapa orang (termasuk saya) menganggapnya sebagai ilmu hitam karena sintaksnya yang tidak intuitif, terutama ketika mulai mengandalkan SFINAE (substitution failure is not an error). SFINAI memang ilmu hitam, karena kode yang nampaknya bakalan compile error ternyata tidak compile error, tapi malah jadi menghilang.

Sisi Gelap C++

Sayangnya, lompatan dari C ke C++ 2003 itu terlalu besar. Saking besarnya, sampai kompleksitasnya juga naik. Ngoding C++ 2003 bisa jadi 2 kali lebih susah dari C. Hanya yang berjuang keras yang berhasil mengendalikan kekuatannya. C++ 2003 tidak hanya terjebak oleh C, dia juga menciptakan masalahnya sendiri. Berikut adalah daftar dosa-dosa C++ 2003

  1. Undefined behavior

    Undefined behavior berarti jika bertemu dengan suatu ekspresi tertentu, maka compiler bebas melakukan apapun, termasuk menghembuskan setan bersayap dari hidung anda. Inilah momok terbesar bagi para pemakai C dan C++. Dengan adanya undefined behavior, maka kelakuan suatu program tidak bisa diprediksi. Kenapa sampai ada undefined behavior? Karena para penulis compiler C dan C++ pengen mengoptimisasi program secara agresif, supaya programnya kenceng. Mari kita lihat beberapa contohnya:

    //Variabel yang tidak diinisialisasi, isinya boleh apa saja
    int x;
    x = x + 27;
    
    //String literal seharusnya tidak boleh dimodifikasi
    char *p = "wikipedia";
    p[0] = 'W'; // undefined behavior
    
    //Pembagian dengan 0 tidak selalu crash atau error
    int x = 1;
    return x / 0; // undefined behavior
    
    //Mengakses elemen di luar array, apapun bisa terjadi
    int arr[4] = {0, 1, 2, 3};
    int *p = arr + 5;
    
    //Bahkan mencobaa mengakses null, bisa saja program seakan-akan normal
    p = 0;
    int a = *p;
    
    

    Sayangnya, contoh-contoh ini hanya sebagian kecil saja dari daftar undefined behavior c++. Jadi, bisa anda bayangkan betapa frustrasinya seorang programmer C ataupun C++ menghadap ranjau undefined behavior. Biasanya sih, compiler modern sudah memperingatkan kasus-kasus undefined behavior yang sering terjadi. Kalau anda mengaku programmer C++ tapi suka mengabaikan compiler warning, berhenti aja jadi programmer C++. Anda tidak layak.

  2. Sintaks template yang menyakitkan

    Jika dilihat, sintaks template memang lebih asing dan lebih terkesan menyeramkan. Inilah yang membuat template dijauhi oleh programmer C++ pemula, apalagi jika mereka mulai belajar dari C. Sayangnya, tidak bisa tidak, untuk bisa jadi programmer C++ yang efektif, harus menguasai template. Lihat saja di atas. Baru mau ngomongin stack, sudah ketemu template. Sebel kan?

    Tidak hanya itu, template ternyata juga susah dipakai. Alasannya macem-macem:

    1. Template harus semuanya di header file
    2. Template compile error nya menakutkan
    3. Template bisa membuat binary jadi membengkak
    4. Template membuat waktu compile jadi lama
  3. Include hell

    Header file adalah sisa-sisa peradaban primitif dari bahasa purba C. C dan C++ memisahkan antara deklarasi dan definisi. Pada umumnya, deklarasi akan ditaruh di header file sementara definisi akan ditaruh di source file. Header file ini akan di-include oleh source file, yaitu sebuah teknik di bahasa C dan C++ untuk menyalin isi dari header file ke dalam source file. Header file ini juga akan di-include oleh file-file lain yang akan memakainya. Header file adalah daftar deklarasi, daftar nama variabel dan fungsi, termasuk juga daftar parameter fungsi dan apa nilai kembaliannya. Oh ya, header file ini tidak boleh di-include dua kali, yang mengakibatkan muncullah keharusan menggunakan header inclusion guard dengan #ifdef.

    //deklarasi variabel
    extern float f;
    
    //definisi variabel
    float f;
    
    //deklarasi sekaligus definisi variabel, kalo extern gak pernah ada.
    //anda bingung? saya juga
    float f;
    
    //deklarasi fungsi
    int square(int x);
    
    //definisi fungsi, ada isi fungsinya
    int square(int x)
    {
        /*...*/
    }
    
    //deklarasi sekaligus definisi fungsi, kalo deklarasi gak pernah ada
    int square(int x)
    {
        /*...*/
    }
    
    //deklarasi nama klas saja
    class SomeClass;
    
    //deklarasi klas SomeClass
    //header file .h untuk klas SomeClass biasanya berisi deklarasi klas seperti ini 
    class SomeClass {
    public:
        static int static_var; //deklarasi variabel static
        void method1(); //deklarasi method
    
        //deklarasi sekaligus definisi (inline) method
        void method2() {
            /*isinya fungsi*/
        }
    };
    
    //definisi method1
    //source file .cpp untuk klas SomeClass biasanya berisi banyak definisi method
    void SomeClass::method1() {
        /*...*/
    }
    
    //definisi variabel static
    //harus ditaruh di file.cpp untuk klas SomeClass
    int SomeClass::static_var;
    

    Sebagai konsekuensi dibangun di atas C, C++ juga harus mengikuti aturan file header dan file source. Cilakanya, C++ itu OOP, yang mana suka banget dengan yang namanya komposisi atau menggabung beberapa klas jadi satu. Akibatnya, setelah adanya C++, header inclusion jadi masalah yang mengesalkan buat para programmer. Tapi gimana lagi? Menu harian saya ketika ngoding C++ adalah undefined reference.

  4. Inheritance Accessibility Level

    C++ mengenal yang namanya public inheritance, protected inheritance, dan private inheritance. Public inheritance artinya semua yang public di base class akan jadi public. Private inheritance artinya semua yang ada di base class akan jadi private. Kalo and termasuk orang yang suka memanfaatkan inheritance untuk menggabung-gabung beberapa klas jadi satu, mungkin anda akan merasa fitur ini bermanfaat. Tapi, Gang of Four sendiri sudah melarang praktik seperti ini dengan prinsip yang dikenal dengan “Favor composition over inheritance”.

    class A {/*...*/};
    
    //public inheritance
    //semua member A yang public akan jadi public di B
    //semua member A yang protected akan jadi protected di B
    //semua member A yang private tidak bisa diakses oleh B
    class B : public A {/*...*/};
    
    //public inheritance
    //semua member A yang public akan jadi protected di C
    //semua member A yang protected akan jadi protected di C
    //semua member A yang private tidak bisa diakses oleh C
    class C : protected A {/*...*/};
    
    //public inheritance
    //semua member A yang public akan jadi private di D
    //semua member A yang protected akan jadi private di D
    //semua member A yang private tidak bisa diakses oleh D
    class D : private A {/*...*/};
    
  5. Multiple inheritance

    Multiple inheritance juga sebenarnya ada untuk mencampur beberapa klas jadi satu. Tapi, begitu kena masalah “diamond inheritance”, kelar hidup lo. Begitu ternyata base class A bertabrakan dengan base class B, pusinglah kepala kita.

    class Binatang {
    public:
        virtual void bersuara() { cout << ""; }
    };
    
    class Naga: public Binatang {
    public:
        virtual void bersuara() { cout << "meraung"; }
    };
    
    class Keledai: public Binatang {
    public:
        virtual void bersuara() {cout << "meringkik";  }
    };
    
    class KeledaiBersayap: public Naga, public Keledai { };
    

    Nah, sekarang bagaimana caranya sekilas anda tahu KeledaiBersap::bersuara() itu bakalan meraung atau meringkik? Ada solusinya memang, tapi anda pasti sudah pusing duluan.

  6. Implicit single parameter constructor

    Seringkali saya suka sama fitur ini. Karena dengan adanya fitur ini, kita bisa berpura-pura bahwa const char* adalah string. Saya bisa masukkan "apapun" ke dalam tipe data std::string dan compiler tidak akan protes. Padahal mereka berbeda. Tapi, begitu mereka disalahgunakan untuk dua tipe data yang tidak nyambung sama sekali, saya akan tepok jidat, sehingga by default saya akan selalu tulis explicit jika parameter constructor cuma satu.

    //Kalau kita punya klas Kampret yang punya constructor dengan satu parameter
    class Kampret {
    public:
        Kampret(int lamaTidurJam) {}
    };
    
    //dan ada fungsi begini:
    void tangkap(Kampret& siKampret);
    
    //Kita bisa pakai begini nih, karena fungsi tangkap akan membuat objek Kampret dari angka 3
    tangkap(3);
    
  7. Function overloading dan default argument

    Function overloading itu bagus, karena saya tidak perlu mencari nama lain untuk fungsi baru yang melakukan hal yang sama dengan fungsi lama, tapi berbeda parameter. Tapi, jika anda juga memakai default argument bersamaan dengan overloading, anda mencari masalah sendiri. Apalagi jika anda juga membuatnya menjadi function template. Biasanya, jika ada template, saya akan menambahkan overloading jika dan hanya jika template type inference tidak berhasil. Sementara, untuk urusan sehari-hari, kalo bisa saya tidak memakai overloading, tapi default argument. Saya tidak pernah mencampur function overloading dan default argument jadi satu.

    #include <iostream>
    
    using namespace std;
    
    //Misalkan kita punya beberapa klas begini
    class Wedang {};
    class WedangAsem : public Wedang {};
    class WedangBajigur : public Wedang {};
    class WedangRonde : public Wedang {};
    
    //dan punya fungsi seperti ini
    void ngombe(WedangAsem& wedangAsem, unsigned pirangGelas = 1) {
        cout << "ngombe wedang asem " << 1 << " gelas." << endl;
    }
    
    void ngombe(Wedang& wedang) {
        cout << "ngombe wedang embuh pirang gelas." << endl;
    }
    
    
    int main() {
        WedangAsem wedangAsem;
        Wedang& wedang = wedangAsem;
        ngombe(wedang);
        ngombe(wedangAsem);
    }
    
    //outputnya akan jadi begini:
    //ngombe wedang embuh pirang gelas.
    //ngombe wedang asem 1 gelas.
    

    Tau kan, kenapa saya nggak suka mencampur default argument dan function overloading? Ini belum kalo nanti ada function template, implicit single parameter constructor, sama user-defined conversion bercampur jadi satu. Mampus lu.

  8. Mutable

    Tahukah anda, variabel const tidak artinya jika suatu struct membernya ada yang mutable? Benar-benar ini mutable, sangat mengejutkan.

    #include <iostream>
    
    using namespace std;
    
    class person {
    public:    
        unsigned getAge() const {
            return ++age;
        }
    private:
        unsigned mutable age = 1;
    };
    
    int main() {
        const person budi;
    
        cout << budi.getAge() << endl;
        cout << budi.getAge() << endl;
        cout << budi.getAge() << endl;
    }
    //outputnya:
    //2
    //3
    //4
    
  9. Locally declared class

    Tahukah anda, bahwa class bisa dideklarasikan di dalam fungsi? Ngeselin kan, tapi emang bisa.

    #include <iostream>
    
    using namespace std;
    
    int main() {
        class person {
        public:    
            unsigned getAge() const {
                return age;
            }
        private:
            unsigned age = 25;
        };
    
        person budi;
    
        cout << budi.getAge() << endl;
    }
    
  10. Object slicing

    class A {
    public:
        int field1;
    };
    
    class B : public A {
    public:
        float field2;
    };
    
    int main() {
        A a;
        a.field1 = 123;
        B b;
        b.field1 = 456;
        b.field2 = 123.456;
    
        a = b;
    }
    
    

    Apa kira-kira yang akan terjadi? Compile error? Ternyata bukan. Yang terjadi adalah object slicing. Padahal, dulu saya pas pertama kali kena ini pengen melakukan polymorphism. Saya berharap kalo salah jadi compile error, tapi ternyata tidak seperti itu maksudnya.

  11. Static member definition

    Ini adalah yang membuat saya kesandung pertama kali saya belajar C++ dan pengen mengimplementasikan static variable. Setiap member yang dideklarasikan static di header, harus didefinisikan lagi di source file. Bahkan lebih jauh, perbedaan soal deklarasi dan definisi ini masih sering membuat saya tersandung ketika sehari-hari ngoding C++.

Inilah beberapa dosa-dosa C++. Saya sengaja hanya tuliskan sedikit yang berhubungan dengan C, untuk menekankan bahwa sedari awal, jika didesain dengan baik, C++ tidak perlu membuat programmer kerepotan hingga seperti ini. Entah apa yang mendasari desain C++, tapi sintaks-sintaksnya tidak intuitif, sehingga berulang kali saya harus tersandung di lubang yang sama.

Di sambungan tulisan ini, saya akan bercerita soal “Gerakan Perlawanan Terhadap C++: Java”