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.
#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;
}Lingkaran, jari-jari 7
Kotak, sisi 5
turun ke tipe yang tepat : true
nullptr pada tipe yang keliru : trueKeluaran 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.
#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;
}penunjuk ke Lingkaran : true
tipe dinamis bukan Bentuk : false
referensi ke Kotak : true
Lingkaran berbeda dari Kotak : true
objek Bentuk polos : trueBaris 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_castpada kelas dasar tanpa fungsi virtual. Compiler menolaknya dengan pesansource type is not polymorphic, dan perbaikannya memastikan kelas dasar memiliki setidaknya satu fungsi virtual, umumnya destructor virtual. - Melewatkan pemeriksaan
nullptrpada 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 perbandingantypeidlangsung. - Menggantikan fungsi virtual dengan serangkaian pemeriksaan
typeidagar 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_castdantypeiduntuk mengenali tipe dinamis objek saat program berjalan, dan keduanya menuntut kelas dasar polimorfik. dynamic_castmenghasilkannullptrpada kegagalan bentuk pointer dan melemparstd::bad_castpada bentuk referensi.typeiddibandingkan langsung dengan==dan!=, sedangkan teksname()-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#
- dynamic_cast conversion — cppreference — Aturan konversi turun yang diperiksa saat program berjalan.
- typeid expression — cppreference
— Perilaku
typeidpada tipe polimorfik dan nonpolimorfik. - std::type_info — cppreference
— Kelas hasil
typeidbeserta operator perbandingan danname(). - Dynamic casting — LearnCpp
— Panduan
dynamic_castbeserta contohnullptrdanstd::bad_cast. - C++ Dialect Options — GCC
— Dokumentasi opsi
-fno-rttiyang menonaktifkan RTTI.