Hai, Habr. Jika Anda tahu jawaban pertanyaan dalam judul, selamat, Anda tidak perlu artikel ini. Ini ditujukan kepada pemula dalam pemrograman, seperti saya, yang tidak selalu dapat memahami semua seluk beluk C ++ dan bahasa lainnya yang diketik secara mandiri, dan jika mereka bisa, lebih baik untuk belajar dari kesalahan orang lain.
Pada artikel ini, saya tidak hanya akan menjawab pertanyaan " 
Mengapa kita membutuhkan fungsi virtual di C ++ ", tetapi saya akan memberikan contoh dari praktik saya. Untuk jawaban singkat, Anda dapat beralih ke mesin pencari yang menghasilkan sesuatu seperti berikut: " 
Fungsi virtual diperlukan untuk memberikan polimorfisme - salah satu dari tiga paus OOP. Berkat mereka, mesin itu sendiri dapat menentukan jenis objek dengan pointer tanpa memuat tugas kepada programmer. " Oke, tetapi pertanyaan "mengapa" tetap ada, meskipun sekarang artinya sedikit berbeda: " 
Mengapa mengandalkan mesin, menghabiskan waktu dan memori ekstra, jika Anda dapat podcast pointer sendiri, karena jenis objek yang dirujuk hampir selalu dikenal? " Memang, casting sekilas meninggalkan fungsi virtual tanpa kerja, dan inilah yang menyebabkan kesalahpahaman dan kode buruk. Dalam proyek-proyek kecil, kerugiannya tidak terlihat, tetapi, seperti yang akan segera Anda lihat, dengan pertumbuhan program, kasta meningkatkan daftar dalam perkembangan yang hampir geometris.
Pertama, mari kita ingat di mana kasta dan fungsi virtual mungkin diperlukan sama sekali. Tipe hilang ketika suatu objek yang dideklarasikan dengan tipe A dialokasikan operasi baru untuk mengalokasikan memori untuk objek tipe B yang kompatibel dengan tipe A, biasanya diwarisi dari A. Paling sering objek bukan satu, tetapi seluruh array. Array pointer dari tipe yang sama, masing-masing menunggu penugasan area memori dengan objek dari tipe yang sama sekali berbeda. Berikut adalah contoh yang akan kami pertimbangkan.
Saya tidak akan berlama-lama, tugasnya adalah ini: berdasarkan pada dokumen yang ditandai dengan bahasa markup Markedit hypertext (Anda dapat membacanya di 
sini ), membangun parsing tree dan membuat file yang berisi dokumen yang sama dalam markup HTML. Solusi saya terdiri dari tiga rutinitas berurutan: mem-parsing teks sumber menjadi token, membangun pohon sintaks dari token dan membangun dokumen HTML berdasarkan itu. Kami tertarik pada bagian kedua.
Faktanya adalah bahwa node dari pohon tujuan memiliki jenis yang berbeda (bagian, paragraf, simpul teks, tautan, catatan kaki, dll.), Tetapi untuk simpul induk, pointer ke simpul anak disimpan dalam larik, dan karenanya memiliki satu jenis - Node.
Parser itu sendiri, dalam bentuk yang disederhanakan, berfungsi seperti ini: ia menciptakan "root" dari pohon sintaksis 
pohon dengan tipe 
Root , menyatakan pointer 
open_node dari tipe umum 
Node , yang segera menetapkan alamat 
tree , dan variabel 
tipe dari tipe yang 
disebutkan , 
Node_type , dan kemudian loop dimulai, 
mengulangi token dari awal. sampai yang terakhir. Pada setiap iterasi, jenis 
open_node open node dimasukkan ke dalam variabel tipe 
pertama (tipe dalam bentuk enumerasi disimpan dalam struktur node), diikuti oleh 
pernyataan switch , yang memeriksa jenis token berikutnya (jenis token sudah dengan hati-hati disediakan oleh lexer). Di setiap cabang switch, cabang lain disajikan yang memeriksa variabel 
tipe , di mana, seperti yang kita ingat, jenis node terbuka terkandung. Bergantung pada nilainya, tindakan yang berbeda dilakukan, misalnya: menambahkan simpul-daftar dari jenis tertentu ke simpul terbuka, membuka simpul lain dari jenis tertentu dalam simpul terbuka dan meneruskan alamatnya ke 
open_node , menutup simpul terbuka, melempar pengecualian. Berlaku untuk topik artikel, kami tertarik pada contoh kedua. Setiap node terbuka (dan umumnya setiap node yang dapat dibuka) sudah berisi array pointer ke node tipe 
Node . Oleh karena itu, ketika kita membuka simpul baru di simpul terbuka (kita menetapkan area memori untuk objek tipe lain ke pointer array berikutnya), untuk penganalisis semantik C ++, ia tetap merupakan instance dari tipe 
Node tanpa memperoleh bidang dan metode baru. Pointer ke sana sekarang ditugaskan ke variabel 
open_node , tanpa kehilangan jenis 
Node . Tetapi bagaimana cara bekerja dengan pointer dari tipe 
Node umum ketika Anda perlu memanggil metode, misalnya, paragraf? Misalnya, 
open_bold () , yang membuka simpul font tebal di dalamnya? Bagaimanapun, 
open_bold () dideklarasikan dan didefinisikan sebagai metode kelas 
Paragraph , dan 
Node sama sekali tidak menyadarinya. Selain itu, 
open_node juga dideklarasikan sebagai pointer ke 
Node , dan harus menerima metode dari semua jenis node pembuka.
Ada dua solusi di sini: yang jelas dan yang tepat. Jelas untuk pemula adalah 
static_cast , dan fungsi virtual benar. Pertama mari kita lihat satu cabang parser switch yang ditulis menggunakan metode pertama:
case Lexer::BOLD_START: { if (type == Node::ROOT) { open_node = tree->open_section(); open_node = static_cast<Section*>(open_node)->open_paragraph(); open_node = static_cast<Paragraph*>(open_node)->open_bold(); } else if (type == Node::SECTION) { open_node = static_cast<Section*>(open_node)->open_paragraph(); open_node = static_cast<Paragraph*>(open_node)->open_bold(); } else if (type == Node::PARAGRAPH) open_node = static_cast<Paragraph*>(open_node)->open_bold(); else if (type == Node::TITLE) open_node = static_cast<Title*>(open_node)->open_bold(); else if (type == Node::QUOTE) open_node = static_cast<Quote*>(open_node)->open_bold(); else if (type == Node::UNORDERED_LIST) { open_node = static_cast<Unordered_list*>(open_node)->close(); while (open_node->get_type() != Node::SECTION) { if (open_node->get_type() == Node::UNORDERED_LIST) open_node = static_cast<Unordered_list*>(open_node)->close(); else if (open_node->get_type() == Node::UNORDERED_LIST) open_node = static_cast<Unordered_list*>(open_node)->close(); else if (open_node->get_type() == Node::PARAGRAPH) open_node = static_cast<Paragraph*>(open_node)->close(); } open_node = static_cast<Section*>(open_node)->open_paragraph(); open_node = static_cast<Paragraph*>(open_node)->open_bold(); } else if (type == Node::ORDERED_LIST) { open_node = static_cast<Ordered_list*>(open_node)->close(); while (open_node->get_type() != Node::SECTION) { if (open_node->get_type() == Node::UNORDERED_LIST) open_node = static_cast<Unordered_list*>(open_node)->close(); else if (open_node->get_type() == Node::UNORDERED_LIST) open_node = static_cast<Unordered_list*>(open_node)->close(); else if (open_node->get_type() == Node::PARAGRAPH) open_node = static_cast<Paragraph*>(open_node)->close(); } open_node = static_cast<Section*>(open_node)->open_paragraph(); open_node = static_cast<Paragraph*>(open_node)->open_bold(); } else if (type == Node::LINK) open_node = static_cast<Link*>(open_node)->open_bold(); else  
Tidak buruk. Dan sekarang, saya tidak akan menyeretnya untuk waktu yang lama, saya akan menunjukkan bagian kode yang sama yang ditulis menggunakan fungsi virtual:
  case Lexer::BOLD_START: { if (type == Node::ROOT) { open_node = tree->open_section(); open_node = open_node->open_paragraph(); open_node = open_node->open_bold(); } else if (type == Node::SECTION) { open_node = open_node->open_paragraph(); open_node = open_node->open_bold(); } else if (type == Node::UNORDERED_LIST) { open_node = open_node->close(); while (open_node->get_type() != Node::SECTION) open_node = open_node->close(); open_node = open_node->open_paragraph(); open_node = open_node->open_bold(); } else  
Keuntungannya jelas, tetapi apakah kita benar-benar membutuhkannya? Setelah semua, maka Anda harus mendeklarasikan dalam kelas 
Node semua metode dari semua kelas turunan sebagai virtual dan entah bagaimana mengimplementasikannya di setiap kelas turunan. Jawabannya adalah ya. Tidak ada banyak metode khusus dalam program ini (29), dan implementasinya dalam kelas turunan yang tidak terkait dengannya hanya terdiri dari satu baris: 
throw string ("error!"); . Anda dapat mengaktifkan mode kreatif dan menghasilkan garis unik untuk setiap lemparan pengecualian. Tetapi yang paling penting - karena pengurangan kode, jumlah kesalahan di dalamnya telah menurun. Casting adalah salah satu penyebab kesalahan kode yang paling penting. Karena setelah menerapkan 
static_cast, kompiler berhenti bersumpah jika kelas yang dipanggil terkandung dalam kelas yang diberikan. Sementara itu, kelas yang berbeda mungkin berisi metode yang berbeda dengan nama yang sama. Dalam kasus saya, 6 disembunyikan dalam kode !!! kesalahan, sementara salah satu dari mereka diduplikasi di beberapa cabang switch. Ini dia:
 else if (type == Node:: open_node = static_cast<Title*>(open_node)->open_italic(); 
Selanjutnya, di bawah spoiler, saya membawa daftar lengkap parser versi pertama dan kedua.
Parser dengan casting Root * Parser::parse (const Lexer &lexer) { Node * open_node(tree); Node::Node_type type; for (unsigned long i(0), len(lexer.count()); i < len; i++) { type = open_node->get_type(); if (type == Node::CITE || type == Node::TEXT || type == Node::NEWLINE || type == Node::NOTIFICATION || type == Node::IMAGE) throw string("error!"); switch (lexer[i].type) { case Lexer::NEWLINE: { if (type == Node::ROOT || type == Node::SECTION) ; else if (type == Node::PARAGRAPH) open_node = static_cast<Paragraph*>(open_node)->add_text("\n"); else if (type == Node::TITLE) open_node = static_cast<Title*>(open_node)->add_text("\n"); else if (type == Node::QUOTE) open_node = static_cast<Quote*>(open_node)->add_text("\n"); else if (type == Node::UNORDERED_LIST) { open_node = static_cast<Unordered_list*>(open_node)->close(); while (open_node->get_type() != Node::SECTION) { if (open_node->get_type() == Node::UNORDERED_LIST) open_node = static_cast<Unordered_list*>(open_node)->close(); else if (open_node->get_type() == Node::UNORDERED_LIST) open_node = static_cast<Unordered_list*>(open_node)->close(); else if (open_node->get_type() == Node::PARAGRAPH) open_node = static_cast<Paragraph*>(open_node)->close(); } } else if (type == Node::ORDERED_LIST) { open_node = static_cast<Ordered_list*>(open_node)->close(); while (open_node->get_type() != Node::SECTION) { if (open_node->get_type() == Node::UNORDERED_LIST) open_node = static_cast<Unordered_list*>(open_node)->close(); else if (open_node->get_type() == Node::UNORDERED_LIST) open_node = static_cast<Unordered_list*>(open_node)->close(); else if (open_node->get_type() == Node::PARAGRAPH) open_node = static_cast<Paragraph*>(open_node)->close(); } } else if (type == Node::LINK) { open_node = static_cast<Link*>(open_node)->add_text(lexer[i].lexeme); } else  
 Parser dengan akses ke metode virtual Root * Parser::parse (const Lexer &lexer) { Node * open_node(tree); Node::Node_type type; for (unsigned long i(0), len(lexer.count()); i < len; i++) { type = open_node->get_type(); if (type == Node::CITE || type == Node::TEXT || type == Node::NEWLINE || type == Node::NOTIFICATION || type == Node::IMAGE) throw string("error!"); switch (lexer[i].type) { case Lexer::NEWLINE: { if (type == Node::ROOT || type == Node::SECTION) ; else if (type == Node::PARAGRAPH || type == Node::TITLE || type == Node::QUOTE || type == Node::TITLE || type == Node::QUOTE) open_node = open_node->add_text("\n"); else if (type == Node::UNORDERED_LIST || type == Node::ORDERED_LIST) { open_node = open_node->close(); while (open_node->get_type() != Node::SECTION) open_node = open_node->close(); } else  
 Dari 1357 baris, kode dikurangi menjadi 487 - hampir tiga kali, tidak termasuk panjang garis!Masih ada satu pertanyaan: bagaimana dengan lead time? Berapa milidetik yang harus kita bayarkan untuk komputer itu sendiri untuk menentukan jenis simpul terbuka? Saya melakukan percobaan - saya memperbaiki waktu kerja parser dalam milidetik dalam kasus pertama dan kedua untuk dokumen yang sama di komputer rumah saya. Inilah hasilnya:Casting - 538 ms.Fungsi virtual - 1174 ms.Total, 636 ms - biaya untuk kekompakan kode dan tidak adanya kesalahan. Apakah ini banyak? Mungkin Tetapi jika kita membutuhkan program yang bekerja secepat mungkin dan membutuhkan memori sesedikit mungkin, kita tidak akan pergi ke OOP dan menulisnya dalam bahasa assembly bersama, menghabiskan waktu seminggu dan mengambil risiko membuat sejumlah besar kesalahan. Jadi pilihan saya adalah di mana static_cast dan dynamic_cast bertemu di program , ganti dengan fungsi virtual. Apa pendapat anda