Mengapa 'polimorfisme murni' lebih disukai daripada menggunakan RTTI?

106

Hampir setiap sumber C ++ yang pernah saya lihat yang membahas hal semacam ini memberi tahu saya bahwa saya harus lebih memilih pendekatan polimorfik daripada menggunakan RTTI (identifikasi jenis run-time). Secara umum, saya menanggapi nasihat semacam ini dengan serius, dan akan mencoba dan memahami alasannya - lagipula, C ++ adalah binatang buas yang perkasa dan sulit dipahami secara mendalam. Namun, untuk pertanyaan khusus ini, saya masih bingung dan ingin melihat nasihat seperti apa yang dapat ditawarkan internet. Pertama, izinkan saya merangkum apa yang telah saya pelajari sejauh ini, dengan membuat daftar alasan umum yang dikutip mengapa RTTI "dianggap berbahaya":

Beberapa kompiler tidak menggunakannya / RTTI tidak selalu diaktifkan

Saya benar-benar tidak percaya argumen ini. Ini seperti mengatakan saya tidak boleh menggunakan fitur C ++ 14, karena ada kompiler di luar sana yang tidak mendukungnya. Namun, tidak ada yang akan melarang saya menggunakan fitur C ++ 14. Mayoritas proyek akan memiliki pengaruh atas kompiler yang mereka gunakan, dan bagaimana itu dikonfigurasi. Bahkan mengutip halaman manual gcc:

-fno-rtti

Nonaktifkan pembuatan informasi tentang setiap kelas dengan fungsi virtual untuk digunakan oleh fitur identifikasi tipe run-time C ++ (dynamic_cast dan typeid). Jika Anda tidak menggunakan bagian-bagian bahasa tersebut, Anda dapat menghemat ruang dengan menggunakan bendera ini. Perhatikan bahwa penanganan pengecualian menggunakan informasi yang sama, tetapi G ++ membuatnya sesuai kebutuhan. Operator dynamic_cast masih dapat digunakan untuk cast yang tidak memerlukan informasi jenis run-time, misalnya cast ke "void *" atau kelas dasar yang tidak ambigu.

Hal ini memberi tahu saya bahwa jika saya tidak menggunakan RTTI, saya dapat menonaktifkannya. Itu seperti mengatakan, jika Anda tidak menggunakan Boost, Anda tidak perlu menautkannya. Saya tidak perlu merencanakan kasus di mana seseorang sedang menyusun -fno-rtti. Plus, kompiler akan gagal dengan keras dan jelas dalam kasus ini.

Ini membutuhkan memori ekstra / Bisa lambat

Setiap kali saya tergoda untuk menggunakan RTTI, itu berarti saya perlu mengakses beberapa jenis informasi atau sifat kelas saya. Jika saya menerapkan solusi yang tidak menggunakan RTTI, ini biasanya berarti saya harus menambahkan beberapa bidang ke kelas saya untuk menyimpan informasi ini, jadi argumen memori agak kosong (saya akan memberikan contoh ini lebih jauh).

Sebuah dynamic_cast memang bisa lambat. Biasanya ada cara untuk menghindari menggunakannya dalam situasi kritis kecepatan. Dan saya tidak melihat alternatifnya. Jawaban SO ini menyarankan penggunaan enum, yang didefinisikan di kelas dasar, untuk menyimpan tipe. Itu hanya berfungsi jika Anda mengetahui semua kelas turunan apriori Anda. Itu cukup besar "jika"!

Dari jawaban tersebut, tampaknya juga biaya RTTI juga tidak jelas. Orang yang berbeda mengukur hal yang berbeda.

Desain polimorfik yang elegan akan membuat RTTI tidak diperlukan

Ini adalah jenis nasihat yang saya anggap serius. Dalam kasus ini, saya tidak dapat menemukan solusi non-RTTI yang baik yang mencakup kasus penggunaan RTTI saya. Izinkan saya memberikan contoh:

Katakanlah saya sedang menulis perpustakaan untuk menangani grafik dari beberapa jenis objek. Saya ingin mengizinkan pengguna untuk membuat tipe mereka sendiri saat menggunakan perpustakaan saya (jadi metode enum tidak tersedia). Saya memiliki kelas dasar untuk node saya:

class node_base
{
  public:
    node_base();
    virtual ~node_base();

    std::vector< std::shared_ptr<node_base> > get_adjacent_nodes();
};

Sekarang, node saya bisa dari berbagai jenis. Bagaimana dengan ini:

class red_node : virtual public node_base
{
  public:
    red_node();
    virtual ~red_node();

    void get_redness();
};

class yellow_node : virtual public node_base
{
  public:
    yellow_node();
    virtual ~yellow_node();

    void set_yellowness(int);
};

Sial, mengapa tidak salah satu dari ini:

class orange_node : public red_node, public yellow_node
{
  public:
    orange_node();
    virtual ~orange_node();

    void poke();
    void poke_adjacent_oranges();
};

Fungsi terakhir menarik. Berikut cara untuk menulisnya:

void orange_node::poke_adjacent_oranges()
{
    auto adj_nodes = get_adjacent_nodes();
    foreach(auto node, adj_nodes) {
        // In this case, typeid() and static_cast might be faster
        std::shared_ptr<orange_node> o_node = dynamic_cast<orange_node>(node);
        if (o_node) {
             o_node->poke();
        }
    }
}

Ini semua tampak jelas dan bersih. Saya tidak perlu mendefinisikan atribut atau metode di mana saya tidak membutuhkannya, kelas simpul dasar dapat tetap ramping dan kejam. Tanpa RTTI, dari mana saya harus memulai? Mungkin saya bisa menambahkan atribut node_type ke kelas dasar:

class node_base
{
  public:
    node_base();
    virtual ~node_base();

    std::vector< std::shared_ptr<node_base> > get_adjacent_nodes();

  private:
    std::string my_type;
};

Apakah std :: string merupakan ide yang bagus untuk suatu tipe? Mungkin tidak, tapi apa lagi yang bisa saya gunakan? Buat nomor dan harap belum ada orang lain yang menggunakannya? Juga, dalam kasus orange_node saya, bagaimana jika saya ingin menggunakan metode dari red_node dan yellow_node? Apakah saya harus menyimpan beberapa jenis per node? Sepertinya itu rumit.

Kesimpulan

Contoh ini tampaknya tidak terlalu rumit atau tidak biasa (saya sedang mengerjakan sesuatu yang serupa di pekerjaan saya, di mana node mewakili perangkat keras sebenarnya yang dikontrol melalui perangkat lunak, dan yang melakukan hal yang sangat berbeda tergantung pada apa mereka). Namun saya tidak tahu cara yang bersih untuk melakukan ini dengan templat atau metode lain. Harap dicatat bahwa saya mencoba untuk memahami masalahnya, bukan membela teladan saya. Saya membaca halaman-halaman seperti jawaban SO yang saya tautkan di atas dan halaman di Wikibooks ini sepertinya menyarankan saya menyalahgunakan RTTI, tetapi saya ingin mengetahui alasannya.

Jadi, kembali ke pertanyaan awal saya: Mengapa 'polimorfisme murni' lebih disukai daripada menggunakan RTTI?

mbr0wn.dll
sumber
9
Apa yang Anda "hilang" (sebagai fitur bahasa) untuk memecahkan contoh jeruk poke Anda adalah pengiriman ganda ("multimethods"). Maka dari itu, mencari cara meniru yang bisa menjadi alternatif. Biasanya, pola pengunjung digunakan karena itu.
Daniel Jour
1
Menggunakan string sebagai tipe tidak terlalu membantu. Menggunakan pointer ke sebuah instance dari beberapa kelas "type" akan membuat ini lebih cepat. Tapi pada dasarnya Anda melakukan secara manual apa yang akan dilakukan RTTI.
Daniel Jour
4
@MargaretBloom Tidak, tidak akan, RTTI adalah singkatan dari Runtime Type Information sedangkan CRTP hanya untuk template - jenis statis, jadi.
edmz
2
@ mbr0wn: semua proses teknik terikat oleh beberapa aturan; pemrograman tidak terkecuali. Aturan dapat dibagi menjadi dua kelompok: aturan lunak (HARUS) dan aturan keras (HARUS). (Ada juga wadah saran / opsi (BISA), begitulah.) Baca bagaimana standar C / C ++ (atau standar bahasa Inggris lainnya, untuk fakta) mendefinisikannya. Saya kira masalah Anda berasal dari fakta bahwa Anda telah salah mengira "jangan gunakan RTTI" sebagai aturan keras ("Anda TIDAK menggunakan RTTI"). Ini sebenarnya aturan lunak ("Anda TIDAK HARUS menggunakan RTTI"), yang berarti bahwa Anda harus menghindarinya kapan pun memungkinkan - dan hanya gunakan jika Anda tidak dapat menghindarinya
3
Saya perhatikan banyak jawaban tidak mencatat gagasan bahwa contoh Anda menyarankan node_baseadalah bagian dari perpustakaan dan pengguna akan membuat jenis simpul mereka sendiri. Kemudian mereka tidak dapat mengubah node_baseuntuk mengizinkan solusi lain, jadi mungkin RTTI menjadi pilihan terbaik mereka. Di sisi lain, ada cara lain untuk mendesain pustaka semacam itu sehingga tipe node baru dapat masuk dengan lebih elegan tanpa perlu menggunakan RTTI (dan cara lain untuk mendesain tipe node baru juga).
Matthew Walton

Jawaban:

69

Antarmuka menjelaskan apa yang perlu diketahui untuk berinteraksi dalam situasi tertentu dalam kode. Setelah Anda memperluas antarmuka dengan "seluruh tipe hierarki", "luas permukaan" antarmuka Anda menjadi sangat besar, yang membuat penalaran tentang hal itu lebih sulit .

Sebagai contoh, "colek jeruk yang berdekatan" berarti saya, sebagai pihak ke-3, tidak dapat meniru menjadi jeruk! Anda secara pribadi menyatakan tipe oranye, lalu menggunakan RTTI untuk membuat kode Anda berperilaku khusus saat berinteraksi dengan tipe itu. Jika saya ingin "menjadi oranye", saya harus berada di dalam taman pribadi Anda.

Sekarang semua orang yang berpasangan dengan pasangan "oranye" dengan seluruh tipe jeruk Anda, dan secara implisit dengan seluruh taman pribadi Anda, bukan dengan antarmuka yang ditentukan.

Meskipun pada pandangan pertama ini tampak seperti cara yang bagus untuk memperluas antarmuka terbatas tanpa harus mengubah semua klien (menambahkan am_I_orange), yang cenderung terjadi justru memperkuat basis kode, dan mencegah ekstensi lebih lanjut. Warna oranye khusus menjadi melekat pada fungsi sistem, dan mencegah Anda membuat pengganti "jeruk keprok" untuk jeruk yang diterapkan secara berbeda dan mungkin menghilangkan ketergantungan atau memecahkan beberapa masalah lain dengan elegan.

Ini berarti antarmuka Anda harus memadai untuk menyelesaikan masalah Anda. Dari perspektif itu, mengapa Anda hanya perlu mencolek jeruk, dan jika demikian, mengapa warna oranye tidak tersedia di antarmuka? Jika Anda memerlukan beberapa set tag kabur yang dapat ditambahkan ad-hoc, Anda dapat menambahkannya ke jenis Anda:

class node_base {
  public:
    bool has_tag(tag_name);

Ini memberikan perluasan antarmuka yang sangat besar yang serupa dari yang ditentukan secara sempit menjadi berbasis tag yang luas. Kecuali melakukannya melalui RTTI dan detail implementasi (alias, "bagaimana Anda diimplementasikan? Dengan tipe oranye? Oke, lulus."), Ia melakukannya dengan sesuatu yang mudah ditiru melalui implementasi yang sama sekali berbeda.

Ini bahkan dapat diperluas ke metode dinamis, jika Anda membutuhkannya. "Apakah Anda mendukung Foo'd dengan argumen Baz, Tom dan Alice? Oke, Fooing Anda." Dalam arti besar, ini kurang intrusif daripada cast dinamis untuk mengetahui fakta bahwa objek lain adalah tipe yang Anda ketahui.

Sekarang objek jeruk keprok dapat memiliki tag oranye dan bermain bersama, sementara implementasi-decoupled.

Ini masih dapat menyebabkan kekacauan besar, tetapi setidaknya ini adalah kekacauan pesan dan data, bukan hierarki implementasi.

Abstraksi adalah permainan memisahkan dan menyembunyikan hal-hal yang tidak relevan. Itu membuat kode lebih mudah untuk bernalar secara lokal. RTTI mengebor lubang langsung melalui abstraksi menjadi detail implementasi. Hal ini dapat membuat pemecahan masalah menjadi lebih mudah, tetapi biaya tersebut membuat Anda terkunci dalam satu implementasi tertentu dengan sangat mudah.

Yakk - Adam Nevraumont
sumber
14
1 untuk paragraf terakhir; bukan hanya karena saya setuju dengan Anda, tetapi karena di sini palu-paku.
7
Bagaimana seseorang mendapatkan fungsionalitas tertentu setelah mengetahui bahwa suatu objek ditandai sebagai pendukung fungsionalitas itu? Entah ini melibatkan casting, atau ada kelas Tuhan dengan setiap fungsi anggota yang memungkinkan. Kemungkinan pertama adalah pengecoran yang tidak dicentang, dalam hal ini penandaan hanyalah skema pemeriksaan jenis dinamis yang sangat salah, atau dicentang dynamic_cast(RTTI), dalam hal ini tag-tag itu berlebihan. Kemungkinan kedua, kelas Dewa, menjijikkan. Kesimpulannya, jawaban ini memiliki banyak kata yang menurut saya terdengar bagus bagi programmer Java, tetapi konten sebenarnya tidak ada artinya.
Cheers and hth. - Alf
2
@Falco: Itu adalah (satu varian dari) kemungkinan pertama yang saya sebutkan, casting yang tidak dicentang berdasarkan tag. Di sini penandaan adalah skema pemeriksaan tipe dinamisnya sendiri yang sangat rapuh dan sangat salah. Setiap kesalahan kode klien kecil, dan di C ++ salah satu tidak aktif di UB-land. Anda tidak mendapatkan pengecualian, seperti yang mungkin Anda dapatkan di Java, tetapi Perilaku Tidak Terdefinisi, seperti error, dan / atau hasil yang salah. Selain sangat tidak dapat diandalkan dan berbahaya, kode ini juga sangat tidak efisien, dibandingkan dengan kode C ++ yang lebih waras. IOW., Itu sangat tidak bagus; sangat begitu.
Cheers and hth. - Alf
1
Uhm. :) Jenis argumen?
Cheers and hth. - Alf
2
@JojOatXGME: Karena "polimorfisme" berarti dapat bekerja dengan berbagai jenis. Jika Anda harus memeriksa apakah itu adalah tipe tertentu , di luar pemeriksaan tipe yang sudah ada yang Anda gunakan untuk mendapatkan penunjuk / referensi untuk memulai, maka Anda melihat di belakang polimorfisme. Anda tidak bekerja dengan berbagai jenis; Anda sedang bekerja dengan tipe tertentu . Ya, ada "proyek (besar) di Java" yang melakukan ini. Tapi itu Java ; bahasa hanya memungkinkan polimorfisme dinamis. C ++ juga memiliki polimorfisme statis. Juga, hanya karena seseorang yang "besar" melakukannya tidak berarti itu ide yang bagus.
Nicol Bolas
31

Sebagian besar bujukan moral terhadap fitur ini atau itu adalah tipikal yang berasal dari pengamatan bahwa ada banyak penggunaan yang salah dari fitur itu.

Di mana para moralis gagal adalah mereka menganggap SEMUA penggunaan salah paham, padahal fitur ada karena suatu alasan.

Mereka memiliki apa yang biasa saya sebut "kompleks tukang ledeng": mereka mengira semua keran tidak berfungsi karena semua keran yang harus diperbaiki berfungsi . Kenyataannya adalah bahwa kebanyakan keran bekerja dengan baik: Anda tidak perlu memanggil tukang ledeng untuk mereka!

Hal gila yang bisa terjadi adalah ketika, untuk menghindari penggunaan fitur tertentu, pemrogram menulis banyak kode boilerplate sebenarnya secara pribadi mengimplementasikan ulang fitur tersebut. (Pernahkah Anda bertemu kelas yang tidak menggunakan RTTI atau panggilan virtual, tetapi memiliki nilai untuk melacak jenis turunan yang sebenarnya? Itu tidak lebih dari penemuan ulang RTTI yang tersamar.)

Ada cara umum untuk berpikir tentang polimorfisme: IF(selection) CALL(something) WITH(parameters). (Maaf, tapi pemrograman, ketika mengabaikan abstraksi, adalah tentang itu)

Penggunaan waktu kompilasi desain (konsep) waktu kompilasi (berbasis template-deduksi), run-time (pewarisan dan berbasis fungsi virtual) atau polimorfisme berbasis data (RTTI dan switching), bergantung pada seberapa banyak keputusan yang diketahui pada setiap tahapan produksi dan bagaimana variabel mereka di setiap konteks.

Idenya adalah:

semakin banyak yang dapat Anda antisipasi, semakin besar peluang untuk menemukan kesalahan dan menghindari bug yang memengaruhi pengguna akhir.

Jika semuanya konstan (termasuk data) Anda dapat melakukan semuanya dengan template meta-programming. Setelah kompilasi terjadi pada konstanta yang diaktualisasikan, seluruh program bermuara pada pernyataan return yang mengeluarkan hasilnya .

Jika ada sejumlah kasus yang semuanya diketahui pada waktu kompilasi , tetapi Anda tidak tahu tentang data aktual yang harus mereka tangani, maka polimorfisme waktu kompilasi (terutama CRTP atau yang serupa) bisa menjadi solusi.

Jika pemilihan kasus bergantung pada data (bukan waktu kompilasi nilai yang diketahui) dan peralihannya mono-dimensi (apa yang harus dilakukan dapat dikurangi menjadi satu nilai saja) maka pengiriman berbasis fungsi virtual (atau secara umum "tabel penunjuk fungsi ") diperlukan.

Jika peralihannya multidimensi, karena tidak ada pengiriman waktu proses multipel bawaan di C ++, Anda harus:

  • Kurangi menjadi satu dimensi dengan Goedelization : di situlah basis virtual dan beberapa warisan, dengan berlian dan jajaran genjang bertumpuk , tetapi ini membutuhkan jumlah kombinasi yang mungkin untuk diketahui dan menjadi relatif kecil.
  • Rantai dimensi satu ke yang lain (seperti dalam pola pengunjung-gabungan, tetapi ini mengharuskan semua kelas untuk mengetahui saudara kandung mereka yang lain, sehingga tidak dapat "menskalakan" dari tempat itu telah disusun)
  • Mengirimkan panggilan berdasarkan beberapa nilai. Itulah gunanya RTTI.

Jika tidak hanya beralih, tetapi bahkan tindakan tidak diketahui waktu kompilasi, maka skrip & parsing diperlukan: data itu sendiri harus menjelaskan tindakan yang akan diambil pada mereka.

Sekarang, karena setiap kasus yang saya sebutkan dapat dilihat sebagai kasus tertentu dari apa yang mengikutinya, Anda dapat menyelesaikan setiap masalah dengan menyalahgunakan solusi paling bawah juga untuk masalah yang terjangkau dengan yang paling atas.

Itulah yang sebenarnya didorong oleh moralisasi untuk dihindari. Tetapi itu tidak berarti bahwa masalah yang tinggal di domain paling bawah tidak ada!

Memukul RTTI hanya untuk menamparnya, sama seperti menampar gotohanya untuk menamparnya. Hal-hal untuk burung beo, bukan pemrogram.

Emilio Garavaglia
sumber
Catatan yang baik tentang tingkat di mana setiap pendekatan dapat diterapkan. Saya belum pernah mendengar tentang "Goedelization" - apakah itu juga dikenal dengan nama lain? Bisakah Anda menambahkan tautan atau penjelasan lebih lanjut? Terima kasih :)
j_random_hacker
1
@j_random_hacker: Saya juga penasaran dengan penggunaan Godelization ini. Orang biasanya menganggap Godelization sebagai yang pertama, memetakan dari beberapa string ke beberapa integer, dan kedua, menggunakan teknik ini untuk menghasilkan pernyataan referensi sendiri dalam bahasa formal. Saya tidak terbiasa dengan istilah ini dalam konteks pengiriman virtual dan ingin mempelajari lebih lanjut.
Eric Lippert
1
Sebenarnya saya menyalahgunakan istilah tersebut: menurut Goedle, karena setiap bilangan bulat sesuai dengan bilangan bulat n-ple (kekuatan faktor prima) dan setiap n-ple sesuai dengan bilangan bulat, setiap masalah pengindeksan dimensi n diskrit dapat direduksi menjadi satu dimensi mono . Itu tidak berarti bahwa ini adalah satu-satunya cara untuk melakukannya: ini hanya cara untuk mengatakan "itu mungkin". Yang Anda butuhkan hanyalah mekanisme "bagi dan taklukkan". fungsi virtual adalah "membagi" dan beberapa warisan adalah "menaklukkan".
Emilio Garavaglia
... Ketika semua yang terjadi di dalam bidang terbatas (rentang) kombinasi linier lebih efektif (klasik i = r * C + c mendapatkan indeks dalam larik sel matriks). Dalam hal ini, bagi id "pengunjung" dan penakluknya adalah "komposit". Karena aljabar linier terlibat, teknik dalam kasus ini sesuai dengan "diagonalisasi"
Emilio Garavaglia
Jangan menganggap semua ini sebagai teknik. Mereka hanyalah analogi
Emilio Garavaglia
23

Ini terlihat agak rapi dalam contoh kecil, tetapi dalam kehidupan nyata Anda akan segera berakhir dengan serangkaian tipe panjang yang dapat saling menyodok, beberapa di antaranya mungkin hanya ke satu arah.

Bagaimana dengan dark_orange_node, atau black_and_orange_striped_node, atau dotted_node? Bisakah itu memiliki titik-titik dengan warna berbeda? Bagaimana jika kebanyakan titik berwarna jingga, apakah bisa ditusuk?

Dan setiap kali Anda harus menambahkan aturan baru, Anda harus mengunjungi kembali semua poke_adjacentfungsi dan menambahkan lebih banyak pernyataan-jika.


Seperti biasa, sulit untuk membuat contoh umum, saya akan berikan itu.

Tetapi jika saya melakukan contoh khusus ini , saya akan menambahkan poke()anggota ke semua kelas dan membiarkan beberapa dari mereka mengabaikan panggilan ( void poke() {}) jika mereka tidak tertarik.

Tentunya itu akan lebih murah daripada membandingkan typeids.

Bo Persson
sumber
3
Anda berkata "pasti", tapi apa yang membuat Anda begitu yakin? Itulah yang saya coba cari tahu. Misalnya saya mengganti nama orange_node menjadi pokable_node, dan hanya merekalah yang dapat saya panggil poke (). Itu berarti antarmuka saya perlu menerapkan metode poke () yang, katakanlah, melempar pengecualian ("node ini tidak dapat ditusuk"). Sepertinya lebih mahal.
mbr0wn
2
Mengapa dia perlu membuat pengecualian? Jika Anda peduli apakah antarmuka "poke -able" atau tidak, tambahkan saja fungsi "isPokeable" dan panggil dulu sebelum memanggil fungsi poke. Atau lakukan saja apa yang dia katakan dan "tidak melakukan apa-apa, di kelas yang tidak bisa disodok".
Brandon
1
@ mbr0wn: Pertanyaan yang lebih baik adalah mengapa Anda ingin node pokable dan nonpokable berbagi kelas dasar yang sama.
Nicol Bolas
2
@NicolBolas Mengapa Anda ingin monster ramah dan bermusuhan berbagi kelas dasar yang sama, atau elemen UI yang dapat difokuskan dan tidak dapat difokuskan, atau keyboard dengan numpad dan keyboard tanpa numpad?
pengguna253751
1
@ mbr0wn Ini terdengar seperti pola-perilaku. Dasar antarmuka memiliki dua metode supportsBehaviourdan invokeBehaviourdan masing-masing kelas dapat memiliki Daftar perilaku. Salah satu perilaku adalah Poke dan dapat ditambahkan ke daftar Perilaku yang didukung oleh semua kelas yang ingin dijadikan pokeable.
Falco
20

Beberapa kompiler tidak menggunakannya / RTTI tidak selalu diaktifkan

Saya yakin Anda telah salah memahami argumen semacam itu.

Ada sejumlah tempat pengkodean C ++ di mana RTTI tidak akan digunakan. Di mana sakelar kompilator digunakan untuk menonaktifkan RTTI secara paksa. Jika Anda membuat kode dalam paradigma seperti itu ... maka Anda hampir pasti sudah diberitahu tentang pembatasan ini.

Karena itu, masalahnya ada pada perpustakaan . Artinya, jika Anda menulis pustaka yang bergantung pada RTTI, pustaka Anda tidak dapat digunakan oleh pengguna yang mematikan RTTI. Jika Anda ingin perpustakaan Anda digunakan oleh orang-orang tersebut, maka perpustakaan tidak dapat menggunakan RTTI, meskipun perpustakaan Anda juga dapat digunakan oleh orang-orang yang dapat menggunakan RTTI. Sama pentingnya, jika Anda tidak dapat menggunakan RTTI, Anda harus berbelanja sedikit lebih keras untuk perpustakaan, karena penggunaan RTTI adalah pemecah kesepakatan bagi Anda.

Ini membutuhkan memori ekstra / Bisa lambat

Ada banyak hal yang tidak Anda lakukan dalam hot loop. Anda tidak mengalokasikan memori. Anda tidak melakukan iterasi melalui daftar tertaut. Dan seterusnya. RTTI tentu saja bisa menjadi salah satu dari hal-hal "jangan lakukan ini di sini".

Namun, pertimbangkan semua contoh RTTI Anda. Dalam semua kasus, Anda memiliki satu atau lebih objek dari tipe tak tentu, dan Anda ingin melakukan beberapa operasi pada mereka yang mungkin tidak dapat dilakukan untuk beberapa di antaranya.

Itu adalah sesuatu yang harus Anda atasi di tingkat desain . Anda dapat menulis container yang tidak mengalokasikan memori yang sesuai dengan paradigma "STL". Anda bisa menghindari struktur data daftar tertaut, atau membatasi penggunaannya. Anda dapat mengatur ulang array dari struct menjadi struct dari array atau apapun. Ini mengubah beberapa hal, tetapi Anda dapat membuatnya tetap terkotak.

Mengubah operasi RTTI yang kompleks menjadi panggilan fungsi virtual biasa? Itu masalah desain. Jika Anda harus mengubahnya, maka itu adalah sesuatu yang membutuhkan perubahan pada setiap kelas turunan. Ini mengubah berapa banyak kode yang berinteraksi dengan berbagai kelas. Cakupan perubahan seperti itu melampaui bagian kode yang sangat penting untuk kinerja.

Jadi ... mengapa Anda menulisnya dengan cara yang salah pada awalnya?

Saya tidak perlu mendefinisikan atribut atau metode di mana saya tidak membutuhkannya, kelas simpul dasar dapat tetap ramping dan kejam.

Ke ujung Apa?

Anda mengatakan bahwa kelas dasarnya adalah "ramping dan kejam". Tapi sungguh ... itu tidak ada . Itu sebenarnya tidak melakukan apa-apa .

Lihat saja contoh Anda: node_base. Apa itu? Tampaknya menjadi sesuatu yang berdekatan dengan hal-hal lain. Ini adalah antarmuka Java (Java pra-generik pada saat itu): kelas yang ada semata-mata untuk menjadi sesuatu yang dapat ditransmisikan pengguna ke tipe sebenarnya . Mungkin Anda menambahkan beberapa fitur dasar seperti adjacency (Java menambahkan ToString), tapi hanya itu.

Ada perbedaan antara "lean and mean" dan "transparent".

Seperti yang dikatakan Yakk, gaya pemrograman seperti itu membatasi dirinya sendiri dalam interoperabilitas, karena jika semua fungsionalitas berada dalam kelas turunan, maka pengguna di luar sistem itu, tanpa akses ke kelas turunan itu, tidak dapat beroperasi dengan sistem. Mereka tidak dapat mengganti fungsi virtual dan menambahkan perilaku baru. Mereka bahkan tidak bisa memanggil fungsi itu.

Tetapi yang juga mereka lakukan adalah membuatnya sangat sulit untuk benar-benar melakukan hal-hal baru, bahkan di dalam sistem. Pertimbangkan poke_adjacent_orangesfungsi Anda . Apa yang terjadi jika seseorang menginginkan lime_nodetipe yang bisa disodok seperti orange_nodes? Nah, kita tidak bisa mendapatkan lime_nodedari orange_node; itu tidak masuk akal.

Sebagai gantinya, kita harus menambahkan lime_nodeturunan baru dari node_base. Kemudian ubah nama poke_adjacent_orangesmenjadi poke_adjacent_pokables. Dan kemudian, coba transmisikan ke orange_nodedan lime_node; pekerjaan cor mana pun adalah yang kami aduk.

Namun, lime_nodekebutuhan itu sendiri poke_adjacent_pokables . Dan fungsi ini perlu melakukan pengecekan casting yang sama.

Dan jika kita menambahkan tipe ketiga, kita tidak hanya harus menambahkan fungsinya sendiri, tetapi kita harus mengubah fungsi di dua kelas lainnya.

Jelas, sekarang Anda membuat poke_adjacent_pokablesfungsi gratis, sehingga berfungsi untuk semuanya. Tetapi menurut Anda apa yang terjadi jika seseorang menambahkan tipe keempat dan lupa menambahkannya ke fungsi itu?

Halo, kerusakan diam-diam . Program tampaknya bekerja lebih atau kurang OK, tetapi sebenarnya tidak. Seandainya pokemerupakan fungsi virtual sebenarnya , kompilator akan gagal jika Anda tidak mengganti fungsi virtual murni dari node_base.

Dengan cara Anda, Anda tidak memiliki pemeriksaan kompiler seperti itu. Tentu saja, kompilator tidak akan memeriksa virtual non-murni, tetapi setidaknya Anda memiliki perlindungan dalam kasus di mana perlindungan dimungkinkan (yaitu: tidak ada operasi default).

Penggunaan kelas dasar transparan dengan RTTI menyebabkan mimpi buruk pemeliharaan. Memang, sebagian besar penggunaan RTTI menyebabkan sakit kepala pemeliharaan. Itu tidak berarti bahwa RTTI tidak berguna (sangat penting untuk membuat boost::anypekerjaan, misalnya). Tetapi ini adalah alat yang sangat khusus untuk kebutuhan yang sangat khusus.

Dengan cara itu, itu "berbahaya" dengan cara yang sama goto. Ini adalah alat berguna yang tidak boleh disingkirkan. Tetapi penggunaannya harus jarang dalam kode Anda.


Jadi, jika Anda tidak dapat menggunakan kelas dasar transparan dan transmisi dinamis, bagaimana Anda menghindari antarmuka yang gemuk? Bagaimana Anda menjaga agar tidak menggelembungkan setiap fungsi yang mungkin ingin Anda panggil pada tipe dari menggelembung ke kelas dasar?

Jawabannya tergantung pada untuk apa kelas dasar itu.

Kelas dasar transparan seperti node_basehanya menggunakan alat yang salah untuk masalah tersebut. Daftar tertaut paling baik ditangani oleh templat. Jenis node dan kedekatan akan disediakan oleh jenis template. Jika Anda ingin memasukkan tipe polimorfik ke dalam daftar, Anda bisa. Gunakan saja BaseClass*seperti Tpada argumen template. Atau penunjuk cerdas pilihan Anda.

Tetapi ada skenario lain. Salah satunya adalah tipe yang melakukan banyak hal, tetapi memiliki beberapa bagian opsional. Contoh tertentu mungkin mengimplementasikan fungsi tertentu, sementara yang lain tidak. Namun, desain tipe seperti itu biasanya menawarkan jawaban yang tepat.

Kelas "entitas" adalah contoh sempurna untuk ini. Kelas ini telah lama mengganggu pengembang game. Secara konseptual, ia memiliki antarmuka raksasa, hidup di persimpangan hampir selusin sistem yang sepenuhnya berbeda. Dan entitas yang berbeda memiliki sifat yang berbeda. Beberapa entitas tidak memiliki representasi visual apa pun, jadi fungsi renderingnya tidak melakukan apa pun. Dan ini semua ditentukan saat runtime.

Solusi modern untuk ini adalah sistem bergaya komponen. Entityhanyalah sebuah wadah dari sekumpulan komponen, dengan sedikit perekat di antara mereka. Beberapa komponen bersifat opsional; entitas yang tidak memiliki representasi visual tidak memiliki komponen "grafik". Entitas tanpa AI tidak memiliki komponen "pengontrol". Dan seterusnya.

Entitas dalam sistem seperti itu hanyalah penunjuk ke komponen, dengan sebagian besar antarmuka mereka disediakan dengan mengakses komponen secara langsung.

Mengembangkan sistem komponen seperti itu memerlukan pengenalan, pada tahap desain, bahwa fungsi tertentu dikelompokkan secara konseptual, sehingga semua jenis yang menerapkan satu akan mengimplementasikan semuanya. Ini memungkinkan Anda untuk mengekstrak kelas dari calon kelas dasar dan menjadikannya komponen terpisah.

Ini juga membantu mengikuti Prinsip Tanggung Jawab Tunggal. Kelas terkomponen seperti itu hanya memiliki tanggung jawab sebagai pemegang komponen.


Dari Matthew Walton:

Saya perhatikan banyak jawaban tidak mencatat gagasan bahwa contoh Anda menyarankan node_base adalah bagian dari perpustakaan dan pengguna akan membuat jenis node mereka sendiri. Kemudian mereka tidak dapat mengubah node_base untuk mengizinkan solusi lain, jadi mungkin RTTI menjadi pilihan terbaik mereka.

Oke, mari kita telusuri itu.

Agar ini masuk akal, yang harus Anda hadapi adalah situasi di mana beberapa perpustakaan L menyediakan wadah atau pemegang data terstruktur lainnya. Pengguna dapat menambahkan data ke wadah ini, mengulangi isinya, dll. Namun, perpustakaan tidak benar-benar melakukan apa pun dengan data ini; ia hanya mengatur keberadaannya.

Tetapi ia bahkan tidak mengelola keberadaannya sebanyak kehancurannya . Alasannya adalah, jika Anda diharapkan menggunakan RTTI untuk tujuan seperti itu, maka Anda membuat kelas yang diabaikan L. Ini berarti kode Anda mengalokasikan objek dan menyerahkannya ke L untuk pengelolaan.

Sekarang, ada kasus di mana sesuatu seperti ini adalah desain yang sah. Pemberian isyarat / pengoperan pesan, antrian kerja thread-safe, dll. Pola umum di sini adalah ini: seseorang melakukan layanan antara dua bagian kode yang sesuai untuk semua jenis, tetapi layanan tidak perlu mengetahui jenis spesifik yang terlibat .

Dalam C, pola ini dieja void*, dan penggunaannya membutuhkan banyak kehati-hatian agar tidak rusak. Dalam C ++, pola ini dieja std::experimental::any(segera dieja std::any).

Cara ini harus bekerja adalah bahwa L menyediakan node_basekelas yang mengambil anyyang mewakili data Anda yang sebenarnya. Saat Anda menerima pesan, item kerja antrian utas, atau apa pun yang Anda lakukan, Anda lalu mentransmisikannya anyke jenis yang sesuai, yang diketahui oleh pengirim dan penerima.

Jadi, bukannya berasal orange_nodedari node_data, Anda hanya tetap sebuah orangedalam node_data's anylapangan anggota. Pengguna akhir mengekstraknya dan menggunakan any_castuntuk mengubahnya menjadi orange. Jika gips gagal, maka tidak orange.

Sekarang, jika Anda sama sekali tidak asing dengan penerapannya any, Anda mungkin akan berkata, "hei tunggu sebentar: gunakan RTTI secara any internal agar any_castberhasil." Yang saya jawab, "... ya".

Itulah inti dari abstraksi . Jauh di dalam detailnya, seseorang menggunakan RTTI. Tetapi pada level tempat Anda seharusnya beroperasi, RTTI langsung bukanlah sesuatu yang harus Anda lakukan.

Anda harus menggunakan tipe yang menyediakan fungsionalitas yang Anda inginkan. Bagaimanapun, Anda tidak benar-benar menginginkan RTTI. Yang Anda inginkan adalah struktur data yang dapat menyimpan nilai dari tipe tertentu, menyembunyikannya dari semua orang kecuali tujuan yang diinginkan, kemudian diubah kembali menjadi tipe itu, dengan verifikasi bahwa nilai yang disimpan sebenarnya dari tipe itu.

Itu namanya any. Ini menggunakan RTTI, tetapi menggunakan anyjauh lebih unggul daripada menggunakan RTTI secara langsung, karena lebih sesuai dengan semantik yang diinginkan.

Nicol Bolas
sumber
10

Jika Anda memanggil suatu fungsi, biasanya Anda tidak terlalu peduli dengan langkah-langkah tepat apa yang akan diambilnya, hanya beberapa tujuan tingkat yang lebih tinggi akan tercapai dalam batasan tertentu (dan bagaimana fungsi membuatnya terjadi sebenarnya adalah masalahnya sendiri).

Saat Anda menggunakan RTTI untuk membuat pemilihan awal objek khusus yang dapat melakukan pekerjaan tertentu, sementara yang lain dalam kumpulan yang sama tidak bisa, Anda melanggar pandangan nyaman tentang dunia. Tiba-tiba si penelepon seharusnya tahu siapa yang bisa melakukan apa, alih-alih hanya memberi tahu antek-anteknya untuk melanjutkan. Beberapa orang merasa terganggu dengan hal ini, dan saya rasa ini adalah sebagian besar alasan mengapa RTTI dianggap sedikit kotor.

Apakah ada masalah kinerja? Mungkin, tapi saya belum pernah mengalaminya, dan mungkin kebijaksanaan dari dua puluh tahun yang lalu, atau dari orang-orang yang secara jujur ​​percaya bahwa menggunakan tiga instruksi perakitan, bukan dua, adalah hal yang tidak dapat diterima.

Jadi bagaimana menghadapinya ... Bergantung pada situasi Anda, mungkin masuk akal untuk memiliki properti khusus node yang digabungkan ke dalam objek terpisah (yaitu seluruh API 'oranye' bisa menjadi objek terpisah). Objek root kemudian dapat memiliki fungsi virtual untuk mengembalikan API 'oranye', mengembalikan nullptr secara default untuk objek non-oranye.

Meskipun ini mungkin berlebihan tergantung pada situasi Anda, ini akan memungkinkan Anda untuk melakukan kueri di tingkat root apakah node tertentu mendukung API tertentu, dan jika ya, menjalankan fungsi yang spesifik untuk API tersebut.

H. Guijt
sumber
6
Re: biaya kinerja - Saya mengukur dynamic_cast <> sebagai biaya sekitar 2μs di aplikasi kami pada prosesor 3GHz, yang sekitar 1000x lebih lambat daripada memeriksa enum. (Aplikasi kami memiliki batas waktu loop utama 11.1ms, jadi kami sangat peduli dengan mikrodetik.)
Crashworks
6
Performa sangat berbeda di antara penerapan. GCC menggunakan perbandingan penunjuk typeinfo yang cepat. MSVC menggunakan perbandingan string yang tidak cepat. Namun, metode MSVC akan bekerja dengan kode yang ditautkan ke berbagai versi pustaka, statis atau DLL, di mana metode penunjuk GCC percaya bahwa kelas di pustaka statis berbeda dari kelas di pustaka bersama.
Zan Lynx
1
@Crashworks Hanya untuk memiliki catatan lengkap di sini: kompiler mana (dan versi mana) itu?
H. Guijt
@Crashworks mendukung permintaan info di mana kompilator menghasilkan hasil pengamatan Anda; Terima kasih.
underscore_d
@underscore_d: MSVC.
Crashworks
9

C ++ dibangun di atas gagasan pemeriksaan tipe statis.

[1] RTTI, yaitu, dynamic_castdan type_id, adalah pemeriksaan tipe dinamis.

Jadi, pada dasarnya Anda bertanya mengapa pemeriksaan tipe statis lebih disukai daripada pemeriksaan tipe dinamis. Dan jawaban sederhananya adalah, apakah pemeriksaan tipe statis lebih disukai daripada pemeriksaan tipe dinamis, tergantung . Banyak. Tetapi C ++ adalah salah satu bahasa pemrograman yang dirancang di sekitar gagasan pemeriksaan tipe statis. Dan ini berarti bahwa misalnya proses pengembangan, khususnya pengujian, biasanya disesuaikan dengan pemeriksaan tipe statis, dan kemudian paling sesuai.


Kembali

Saya tidak akan tahu cara yang bersih untuk melakukan ini dengan templat atau metode lain

Anda dapat melakukan proses ini-heterogen-node-of-a-graph dengan pemeriksaan tipe statis dan tanpa transmisi apapun melalui pola pengunjung, misalnya seperti ini:

#include <iostream>
#include <set>
#include <initializer_list>

namespace graph {
    using std::set;

    class Red_thing;
    class Yellow_thing;
    class Orange_thing;

    struct Callback
    {
        virtual void handle( Red_thing& ) {}
        virtual void handle( Yellow_thing& ) {}
        virtual void handle( Orange_thing& ) {}
    };

    class Node
    {
    private:
        set<Node*> connected_;

    public:
        virtual void call( Callback& cb ) = 0;

        void connect_to( Node* p_other )
        {
            connected_.insert( p_other );
        }

        void call_on_connected( Callback& cb )
        {
            for( auto const p : connected_ ) { p->call( cb ); }
        }

        virtual ~Node(){}
    };

    class Red_thing
        : public virtual Node
    {
    public:
        void call( Callback& cb ) override { cb.handle( *this ); }

        auto redness() -> int { return 255; }
    };

    class Yellow_thing
        : public virtual Node
    {
    public:
        void call( Callback& cb ) override { cb.handle( *this ); }
    };

    class Orange_thing
        : public Red_thing
        , public Yellow_thing
    {
    public:
        void call( Callback& cb ) override { cb.handle( *this ); }

        void poke() { std::cout << "Poked!\n"; }

        void poke_connected_orange_things()
        {
            struct Poker: Callback
            {
                void handle( Orange_thing& obj ) override
                {
                    obj.poke();
                }
            } poker;

            call_on_connected( poker );
        }
    };
}  // namespace graph

auto main() -> int
{
    using namespace graph;

    Red_thing   r;
    Yellow_thing    y1, y2;
    Orange_thing    o1, o2, o3;

    for( Node* p : std::initializer_list<Node*>{ &y1, &y2, &r, &o2, &o3 } )
    {
        o1.connect_to( p );
    }
    o1.poke_connected_orange_things();
}

Ini mengasumsikan bahwa kumpulan jenis node diketahui.

Jika tidak, pola pengunjung (ada banyak variasinya) dapat diekspresikan dengan beberapa pemeran terpusat, atau, hanya satu.


Untuk pendekatan berbasis template, lihat pustaka Boost Graph. Sedih untuk mengatakan saya tidak terbiasa dengan itu, saya belum menggunakannya. Jadi saya tidak yakin persis apa yang dilakukannya dan bagaimana, dan sejauh mana ia menggunakan pemeriksaan tipe statis daripada RTTI, tetapi karena Boost umumnya berbasis template dengan pemeriksaan jenis statis sebagai ide utamanya, saya pikir Anda akan menemukannya sub-library Graphnya juga didasarkan pada pemeriksaan tipe statis.


[1] Jalankan Informasi Jenis Waktu .

Cheers and hth. - Alf
sumber
1
Satu "hal lucu" untuk diperhatikan adalah bahwa seseorang dapat mengurangi jumlah kode (perubahan saat menambahkan jenis) yang diperlukan untuk pola pengunjung adalah dengan menggunakan RTTI untuk "mendaki" hierarki. Saya tahu ini sebagai "pola pengunjung asiklik".
Daniel Jour
3

Tentu saja ada skenario di mana polimorfisme tidak dapat membantu: nama. typeidmemungkinkan Anda mengakses nama jenis, meskipun cara penyandian nama ini ditentukan oleh implementasi. Tetapi biasanya ini bukan masalah karena Anda dapat membandingkan dua typeid-s:

if ( typeid(5) == "int" )
    // may be false

if ( typeid(5) == typeid(int) )
   // always true

Hal yang sama berlaku untuk hash.

[...] RTTI "dianggap berbahaya"

berbahaya jelas melebih-lebihkan: RTTI memiliki beberapa kekurangan, tetapi juga memiliki kelebihan.

Anda tidak benar-benar harus menggunakan RTTI. RTTI adalah alat untuk memecahkan masalah OOP: jika Anda menggunakan paradigma lain, kemungkinan besar ini akan hilang. C tidak memiliki RTTI, tetapi masih berfungsi. C ++ bukan sepenuhnya mendukung OOP dan memberi Anda beberapa alat untuk mengatasi beberapa masalah yang mungkin memerlukan informasi runtime: salah satunya adalah memang RTTI, yang meskipun datang dengan harga. Jika Anda tidak mampu membelinya, hal yang sebaiknya Anda nyatakan hanya setelah analisis kinerja yang aman, masih ada sekolah lama void*: gratis. Gratis. Tapi Anda tidak mendapatkan keamanan tipe. Jadi ini semua tentang perdagangan.


  • Beberapa kompiler tidak menggunakan / RTTI tidak selalu diaktifkan.
    Saya benar-benar tidak percaya argumen ini. Ini seperti mengatakan saya tidak boleh menggunakan fitur C ++ 14, karena ada kompiler di luar sana yang tidak mendukungnya. Namun, tidak ada yang akan melarang saya menggunakan fitur C ++ 14.

Jika Anda menulis (mungkin secara ketat) kode C ++ yang sesuai, Anda dapat mengharapkan perilaku yang sama terlepas dari implementasinya. Implementasi yang sesuai standar harus mendukung fitur C ++ standar.

Tetapi pertimbangkan bahwa dalam beberapa lingkungan yang didefinisikan C ++ (yang «berdiri sendiri»), RTTI tidak perlu disediakan dan juga tidak ada pengecualian, virtualdan seterusnya. RTTI membutuhkan lapisan yang mendasari untuk bekerja dengan benar yang berhubungan dengan detail tingkat rendah seperti ABI dan informasi tipe aktual.


Saya setuju dengan Yakk tentang RTTI dalam kasus ini. Ya, itu bisa digunakan; tetapi apakah itu benar secara logis? Fakta bahwa bahasa memungkinkan Anda melewati pemeriksaan ini tidak berarti itu harus dilakukan.

edmz
sumber