RTTI

dynamic_cast, typeid, dan kapan downcasting menandakan masalah desain.

Daftar isi

RTTI (run-time type information) adalah fasilitas C++ untuk mengenali tipe nyata sebuah objek saat program berjalan, dan topik ini membahas dua perangkat utamanya, yaitu dynamic_cast dan typeid. Materinya mencakup cara kerja konversi turun yang diperiksa saat program berjalan, perbandingan tipe dengan typeid, serta penilaian kapan konversi turun justru menandakan masalah desain. Setelah membacanya, kedua operator tersebut dapat dipakai pada hierarki polimorfik dengan batasannya dipahami dengan tepat.

dynamic_cast untuk konversi turun#

dynamic_cast melakukan konversi turun (downcasting), yaitu mengubah pointer atau referensi kelas dasar menjadi kelas turunan, sambil memeriksa kebenarannya saat program berjalan. Pemeriksaan ini hanya mungkin pada tipe polimorfik, yaitu kelas dasar yang memiliki setidaknya satu fungsi virtual, karena informasi tipe dinamis tersimpan bersama fungsi virtual tersebut. Apabila objeknya memang bertipe yang dituju, hasilnya pointer yang sah, sedangkan apabila tidak, hasilnya nullptr untuk bentuk pointer dan exception std::bad_cast untuk bentuk referensi. Program berikut memperagakan kedua kondisi tersebut.

cpp konversi-turun.cpp
#include <iostream>

class Bentuk {
public:
    virtual ~Bentuk() = default;
};

class Lingkaran : public Bentuk {
public:
    explicit Lingkaran(double r) : jari{r} {}

    double jari;
};

class Kotak : public Bentuk {
public:
    explicit Kotak(double s) : sisi{s} {}

    double sisi;
};

void periksa(const Bentuk* b) {
    if (const Lingkaran* ling = dynamic_cast<const Lingkaran*>(b); ling != nullptr) {
        std::cout << "Lingkaran, jari-jari " << ling->jari << "\n";
    } else if (const Kotak* kotak = dynamic_cast<const Kotak*>(b); kotak != nullptr) {
        std::cout << "Kotak, sisi " << kotak->sisi << "\n";
    } else {
        std::cout << "Bentuk lain\n";
    }
}

int main() {
    Lingkaran ling{7.0};
    Kotak kotak{5.0};

    periksa(&ling);
    periksa(&kotak);

    std::cout << std::boolalpha;
    Bentuk* pb = &ling;
    Lingkaran* tepat = dynamic_cast<Lingkaran*>(pb);
    std::cout << "turun ke tipe yang tepat      : " << (tepat != nullptr) << "\n";

    pb = &kotak;
    Lingkaran* keliru = dynamic_cast<Lingkaran*>(pb);
    std::cout << "nullptr pada tipe yang keliru : " << (keliru == nullptr) << "\n";
    return 0;
}
text Keluaran
Lingkaran, jari-jari 7
Kotak, sisi 5
turun ke tipe yang tepat      : true
nullptr pada tipe yang keliru : true

Keluaran tersebut memperlihatkan dynamic_cast bekerja sesuai tipe dinamis objeknya, bukan tipe pointer yang memegangnya. Percobaan turun ke Lingkaran* pada objek Kotak menghasilkan nullptr tanpa membahayakan program, sehingga hasilnya wajib diperiksa sebelum dipakai. Bentuk referensi tidak memiliki nilai kosong untuk dikembalikan, sehingga kegagalan konversi dilemparkan sebagai exception std::bad_cast dari header <typeinfo> dan ditangkap dengan blok try dan catch apabila bentuk itu dipilih. Konversi naik (upcasting) sebaliknya tidak memerlukan dynamic_cast, karena dari kelas turunan ke kelas dasar cukup konversi implisit atau static_cast. Pemakaian dynamic_cast pada arah itu sah tetapi tidak diperlukan, karena pemeriksaan saat program berjalan lebih mahal daripada konversi yang sudah pasti benar sejak kompilasi.

typeid dan perbandingan tipe#

Operator typeid mengembalikan informasi tipe sebuah ekspresi dalam bentuk referensi std::type_info, dan dua hasilnya dapat dibandingkan langsung dengan == maupun !=. Pada ekspresi bertipe polimorfik yang di-dereference, typeid mengevaluasi tipe dinamisnya saat program berjalan, sedangkan pada objek biasa dan nama tipe, hasilnya ditentukan saat kompilasi. Perbedaan perilaku inilah yang membuat typeid berguna untuk memeriksa tipe nyata objek di balik pointer maupun referensi. Program berikut membandingkan hasil typeid tanpa melibatkan teksnya.

cpp typeid-perbandingan.cpp
#include <iostream>
#include <typeinfo>

class Bentuk {
public:
    virtual ~Bentuk() = default;
};

class Lingkaran : public Bentuk {};
class Kotak : public Bentuk {};

int main() {
    Lingkaran ling;
    Kotak kotak;

    Bentuk* penunjuk = &ling;
    Bentuk& referensi = kotak;
    Bentuk polos;

    std::cout << std::boolalpha;
    std::cout << "penunjuk ke Lingkaran        : " << (typeid(*penunjuk) == typeid(Lingkaran)) << "\n";
    std::cout << "tipe dinamis bukan Bentuk    : " << (typeid(*penunjuk) == typeid(Bentuk)) << "\n";
    std::cout << "referensi ke Kotak           : " << (typeid(referensi) == typeid(Kotak)) << "\n";
    std::cout << "Lingkaran berbeda dari Kotak : " << (typeid(Lingkaran) != typeid(Kotak)) << "\n";
    std::cout << "objek Bentuk polos           : " << (typeid(polos) == typeid(Bentuk)) << "\n";
    return 0;
}
text Keluaran
penunjuk ke Lingkaran        : true
tipe dinamis bukan Bentuk    : false
referensi ke Kotak           : true
Lingkaran berbeda dari Kotak : true
objek Bentuk polos           : true

Baris pertama bernilai true karena typeid(*penunjuk) melihat tipe dinamis Lingkaran, sedangkan baris kedua bernilai false karena tipe dinamis itu memang bukan Bentuk. Anggota name() milik std::type_info memang dapat mencetak nama tipe, tetapi teksnya bergantung compiler. GCC, misalnya, mengembalikan nama yang di-mangle seperti 9Lingkaran dan bukan teks yang mudah dibaca. Perbandingan boolean seperti di atas dengan demikian lebih andal daripada perbandingan teks.

Gunakan name() hanya untuk keperluan log dan debugging, dengan catatan bahwa bentuk teksnya dapat berbeda antarcompiler. Selain itu, evaluasi typeid pada dereference pointer bernilai nullptr melempar exception std::bad_typeid, sehingga pointer yang keabsahannya tidak dijamin tetap diperiksa terlebih dahulu. Operator ini juga tidak terbatas pada hierarki kelas, karena typeid(3.14) == typeid(double) sah dan ditentukan saat kompilasi. Pemeriksaan tipe primitif pun dapat memanfaatkannya.

Kapan downcasting menandakan masalah desain#

Konversi turun yang muncul berulang biasanya menandakan antarmuka kelas dasarnya terlalu sempit. Pola yang paling sering dijumpai adalah rantai dynamic_cast panjang seperti fungsi periksa di atas, yaitu rantai yang diperpanjang setiap kali ada tipe baru, atau pencocokan hasil name() terhadap teks tertentu untuk menentukan perilaku. Kedua pola itu sebaiknya diganti fungsi virtual, karena keputusan berdasarkan tipe pada dasarnya adalah perilaku yang dimiliki objek yang bersangkutan. Dengan fungsi virtual, penambahan kelas turunan baru tidak menuntut perubahan kode pemanggil.

Contoh konkretnya adalah penghitung luas. Apabila fungsi penjumlahan luas memeriksa tipe setiap elemen sebelum menghitung, penambahan satu bentuk baru menuntut pemeriksaan itu diperpanjang. Fungsi virtual luas() pada kelas dasar menyelesaikan hal yang sama tanpa mengubah kode pemanggilnya, dan satu lompatan tabel virtual juga lebih murah daripada serangkaian perbandingan tipe.

Konversi turun tetap beralasan pada situasi yang lebih sempit, misalnya batas antarmuka dengan kode lama yang tidak dapat diubah, kebutuhan memanggil member yang memang hanya ada pada satu kelas turunan, atau basis kode yang menonaktifkan RTTI melalui opsi -fno-rtti karena alasan ukuran biner. Di luar situasi itu, kemunculan dynamic_cast di tengah logika utama layak dibaca sebagai sinyal untuk merapikan hierarkinya terlebih dahulu. Cara mengujinya sederhana, yaitu memeriksa apakah member yang dicari layak dipindahkan menjadi fungsi virtual milik kelas dasar. Apabila jawabannya ya, pemeriksaan tipe itu memang seharusnya tidak ada.

Kesalahan umum#

Kesalahan berikut paling sering muncul ketika RTTI dipakai pertama kali.

  • Memanggil dynamic_cast pada kelas dasar tanpa fungsi virtual. Compiler menolaknya dengan pesan source type is not polymorphic, dan perbaikannya memastikan kelas dasar memiliki setidaknya satu fungsi virtual, umumnya destructor virtual.
  • Melewatkan pemeriksaan nullptr pada hasil bentuk pointer. Program berhenti saat dereference dilakukan, sehingga hasilnya selalu diperiksa seperti pada contoh.
  • Membedakan perilaku melalui teks name(). Teksnya bergantung compiler dan dapat berubah, sehingga keputusan program menggunakan perbandingan typeid langsung.
  • Menggantikan fungsi virtual dengan serangkaian pemeriksaan typeid agar hierarki terasa sederhana. Keputusan perilaku tetap milik objek, sehingga fungsi virtual menjadi pilihan utama, sedangkan pemeriksaan tipe disisakan untuk kasus yang benar-benar memerlukannya.

Ringkasan#

  • RTTI menyediakan dynamic_cast dan typeid untuk mengenali tipe dinamis objek saat program berjalan, dan keduanya menuntut kelas dasar polimorfik.
  • dynamic_cast menghasilkan nullptr pada kegagalan bentuk pointer dan melempar std::bad_cast pada bentuk referensi.
  • typeid dibandingkan langsung dengan == dan !=, sedangkan teks name()-nya bergantung compiler sehingga hanya layak untuk log.
  • Konversi turun yang berulang di tengah logika biasanya menandakan antarmuka terlalu sempit, dan perbaikannya adalah fungsi virtual alih-alih pemeriksaan tipe.

Referensi#